From 5fb0ebcfa0f58c35269fb28a2659ff7ac9e0749a Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Sat, 22 Apr 2023 17:00:26 -0400 Subject: [PATCH 01/16] Add sample Hello World Scheduled Job Signed-off-by: Craig Perkins --- build.gradle | 6 + .../helloworld/HelloWorldExtension.java | 37 ++- .../rest/RestRemoteHelloAction.java | 199 +++++++++++- .../sample/helloworld/schedule/GreetJob.java | 306 ++++++++++++++++++ .../transport/HWJobParameterAction.java | 22 ++ .../HWJobParameterTransportAction.java | 72 +++++ .../transport/HWJobRunnerAction.java | 23 ++ .../transport/HWJobRunnerTransportAction.java | 154 +++++++++ .../transport/HelloWorldJobRunner.java | 40 +++ .../helloworld/util/RestHandlerUtils.java | 83 +++++ .../resources/mappings/hello-world-jobs.json | 50 +++ 11 files changed, 982 insertions(+), 10 deletions(-) create mode 100644 src/main/java/org/opensearch/sdk/sample/helloworld/schedule/GreetJob.java create mode 100644 src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobParameterAction.java create mode 100644 src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobParameterTransportAction.java create mode 100644 src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobRunnerAction.java create mode 100644 src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobRunnerTransportAction.java create mode 100644 src/main/java/org/opensearch/sdk/sample/helloworld/transport/HelloWorldJobRunner.java create mode 100644 src/main/java/org/opensearch/sdk/sample/helloworld/util/RestHandlerUtils.java create mode 100644 src/main/resources/mappings/hello-world-jobs.json diff --git a/build.gradle b/build.gradle index 878a54d3..8aaa8169 100644 --- a/build.gradle +++ b/build.gradle @@ -76,6 +76,7 @@ repositories { dependencies { def opensearchVersion = "3.0.0-SNAPSHOT" + def jobSchedulerVersion = "3.0.0.0-SNAPSHOT" def log4jVersion = "2.20.0" def nettyVersion = "4.1.91.Final" def jacksonDatabindVersion = "2.14.2" @@ -87,6 +88,11 @@ dependencies { def jaxbVersion = "2.3.1" implementation("org.opensearch:opensearch:${opensearchVersion}") + + // job-scheduler dependency is added for hello world sample extension + implementation("org.opensearch.plugin:opensearch-job-scheduler:${jobSchedulerVersion}") + implementation "org.opensearch:opensearch-job-scheduler-spi:${jobSchedulerVersion}" + implementation("org.apache.logging.log4j:log4j-api:${log4jVersion}") implementation("org.apache.logging.log4j:log4j-core:${log4jVersion}") implementation("org.apache.logging.log4j:log4j-jul:${log4jVersion}") diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/HelloWorldExtension.java b/src/main/java/org/opensearch/sdk/sample/helloworld/HelloWorldExtension.java index e3459771..44d4604a 100644 --- a/src/main/java/org/opensearch/sdk/sample/helloworld/HelloWorldExtension.java +++ b/src/main/java/org/opensearch/sdk/sample/helloworld/HelloWorldExtension.java @@ -11,19 +11,29 @@ import java.io.IOException; import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import java.util.List; +import com.google.common.collect.ImmutableList; import org.opensearch.action.ActionRequest; import org.opensearch.action.ActionResponse; import org.opensearch.common.settings.Setting; +import org.opensearch.core.xcontent.NamedXContentRegistry; import org.opensearch.sdk.BaseExtension; import org.opensearch.sdk.Extension; import org.opensearch.sdk.ExtensionSettings; import org.opensearch.sdk.ExtensionsRunner; +import org.opensearch.sdk.SDKClient; import org.opensearch.sdk.api.ActionExtension; import org.opensearch.sdk.rest.ExtensionRestHandler; import org.opensearch.sdk.sample.helloworld.rest.RestHelloAction; import org.opensearch.sdk.sample.helloworld.rest.RestRemoteHelloAction; +import org.opensearch.sdk.sample.helloworld.schedule.GreetJob; +import org.opensearch.sdk.sample.helloworld.transport.HWJobParameterAction; +import org.opensearch.sdk.sample.helloworld.transport.HWJobParameterTransportAction; +import org.opensearch.sdk.sample.helloworld.transport.HWJobRunnerAction; +import org.opensearch.sdk.sample.helloworld.transport.HWJobRunnerTransportAction; import org.opensearch.sdk.sample.helloworld.transport.SampleAction; import org.opensearch.sdk.sample.helloworld.transport.SampleTransportAction; @@ -61,7 +71,32 @@ public List getExtensionRestHandlers() { @Override public List> getActions() { - return Arrays.asList(new ActionHandler<>(SampleAction.INSTANCE, SampleTransportAction.class)); + return Arrays.asList( + new ActionHandler<>(SampleAction.INSTANCE, SampleTransportAction.class), + new ActionHandler<>(HWJobRunnerAction.INSTANCE, HWJobRunnerTransportAction.class), + new ActionHandler<>(HWJobParameterAction.INSTANCE, HWJobParameterTransportAction.class) + ); + } + + @Override + public List getNamedXContent() { + return ImmutableList.of( + GreetJob.XCONTENT_REGISTRY + ); + } + + @Deprecated + private SDKClient.SDKRestClient createRestClient(ExtensionsRunner runner) { + @SuppressWarnings("resource") + SDKClient.SDKRestClient client = runner.getSdkClient().initializeRestClient(); + return client; + } + + @Override + public Collection createComponents(ExtensionsRunner runner) { + SDKClient.SDKRestClient sdkRestClient = createRestClient(runner); + + return Collections.singletonList(sdkRestClient); } /** diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/rest/RestRemoteHelloAction.java b/src/main/java/org/opensearch/sdk/sample/helloworld/rest/RestRemoteHelloAction.java index 69f32dac..a0a29e7f 100644 --- a/src/main/java/org/opensearch/sdk/sample/helloworld/rest/RestRemoteHelloAction.java +++ b/src/main/java/org/opensearch/sdk/sample/helloworld/rest/RestRemoteHelloAction.java @@ -9,34 +9,77 @@ package org.opensearch.sdk.sample.helloworld.rest; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.io.Resources; +import jakarta.json.stream.JsonParser; +import org.apache.commons.codec.Charsets; +import org.opensearch.OpenSearchException; +import org.opensearch.ResourceAlreadyExistsException; import org.opensearch.action.ActionListener; +import org.opensearch.client.Request; +import org.opensearch.client.Response; +import org.opensearch.client.WarningFailureException; +import org.opensearch.client.json.JsonpMapper; +import org.opensearch.client.json.jackson.JacksonJsonpMapper; +import org.opensearch.client.json.jsonb.JsonbJsonpMapper; +import org.opensearch.client.opensearch.OpenSearchClient; +import org.opensearch.client.opensearch._types.mapping.TypeMapping; +import org.opensearch.client.opensearch.core.IndexRequest; +import org.opensearch.client.opensearch.indices.CreateIndexRequest; +import org.opensearch.client.opensearch.indices.GetMappingResponse; +import org.opensearch.client.opensearch.indices.OpenSearchIndicesClient; +import org.opensearch.common.Strings; +import org.opensearch.common.bytes.BytesReference; import org.opensearch.common.io.stream.StreamInput; +import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.common.xcontent.XContentHelper; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.common.xcontent.json.JsonXContent; +import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.extensions.ExtensionsManager; import org.opensearch.extensions.action.RemoteExtensionActionResponse; import org.opensearch.extensions.rest.ExtensionRestResponse; +import org.opensearch.jobscheduler.JobSchedulerPlugin; +import org.opensearch.jobscheduler.rest.request.GetJobDetailsRequest; +import org.opensearch.jobscheduler.spi.schedule.IntervalSchedule; +import org.opensearch.jobscheduler.spi.schedule.Schedule; import org.opensearch.rest.RestRequest; +import org.opensearch.rest.RestStatus; import org.opensearch.sdk.ExtensionsRunner; import org.opensearch.sdk.SDKClient; import org.opensearch.sdk.action.RemoteExtensionAction; import org.opensearch.sdk.action.RemoteExtensionActionRequest; import org.opensearch.sdk.rest.BaseExtensionRestHandler; +import org.opensearch.sdk.sample.helloworld.schedule.GreetJob; +import org.opensearch.sdk.sample.helloworld.transport.HWJobParameterAction; +import org.opensearch.sdk.sample.helloworld.transport.HWJobRunnerAction; import org.opensearch.sdk.sample.helloworld.transport.SampleAction; import org.opensearch.sdk.sample.helloworld.transport.SampleRequest; import org.opensearch.sdk.sample.helloworld.transport.SampleResponse; +import org.opensearch.sdk.sample.helloworld.util.RestHandlerUtils; +import java.io.IOException; +import java.io.StringReader; +import java.net.URL; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Random; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.function.Function; import static org.opensearch.rest.RestRequest.Method.GET; +import static org.opensearch.rest.RestRequest.Method.PUT; import static org.opensearch.rest.RestStatus.OK; /** - * Sample REST Handler demostrating proxy actions to another extension + * Sample REST Handler demonstrating proxy actions to another extension */ public class RestRemoteHelloAction extends BaseExtensionRestHandler { - private ExtensionsRunner extensionsRunner; /** @@ -50,9 +93,147 @@ public RestRemoteHelloAction(ExtensionsRunner runner) { @Override public List routeHandlers() { - return List.of(new RouteHandler(GET, "/hello/{name}", handleRemoteGetRequest)); + return List.of( + new RouteHandler(GET, "/hello/{name}", handleRemoteGetRequest), + new RouteHandler(PUT, "/schedule/hello", handleScheduleRequest) + ); + } + + private void registerJobDetails(SDKClient.SDKRestClient client) throws IOException { + + XContentBuilder requestBody = JsonXContent.contentBuilder(); + requestBody.startObject(); + requestBody.field(GetJobDetailsRequest.JOB_INDEX, GreetJob.HELLO_WORLD_JOB_INDEX); + requestBody.field(GetJobDetailsRequest.JOB_TYPE, GreetJob.PARSE_FIELD_NAME); + requestBody.field(GetJobDetailsRequest.JOB_PARAMETER_ACTION, HWJobParameterAction.class.getName()); + requestBody.field(GetJobDetailsRequest.JOB_RUNNER_ACTION, HWJobRunnerAction.class.getName()); + requestBody.field(GetJobDetailsRequest.EXTENSION_UNIQUE_ID, extensionsRunner.getUniqueId()); + requestBody.endObject(); + + Request request = new Request("PUT", String.format(Locale.ROOT, "%s/%s", JobSchedulerPlugin.JS_BASE_URI, "_job_details")); + request.setJsonEntity(Strings.toString(requestBody)); + + Response response = client.performRequest(request); + boolean registeredJobDetails = RestStatus.fromCode(response.getStatusLine().getStatusCode()) == RestStatus.OK ? true : false; + } + + /** + * Get hello world job index mapping json content. + * + * @return hello world job index mapping + * @throws IOException IOException if mapping file can't be read correctly + */ + public static String getHelloWorldJobMappings() throws IOException { + URL url = RestRemoteHelloAction.class.getClassLoader().getResource("mappings/hello-world-jobs.json"); + return Resources.toString(url, Charsets.UTF_8); + } + + private JsonpMapper setupMapper(int rand) { + // Randomly choose json-b or jackson + if (rand % 2 == 0) { + return new JsonbJsonpMapper() { + @Override + public boolean ignoreUnknownFields() { + return false; + } + }; + } else { + return new JacksonJsonpMapper() { + @Override + public boolean ignoreUnknownFields() { + return false; + } + }; + } } + public T fromJson(String json, Class clazz) { + int rand = new Random().nextInt(); + JsonpMapper mapper = setupMapper(rand); + JsonParser parser = mapper.jsonProvider().createParser(new StringReader(json)); + return mapper.deserialize(parser, clazz); + } + + private Function handleScheduleRequest = (request) -> { + SDKClient client = extensionsRunner.getSdkClient(); + SDKClient.SDKRestClient restClient = client.initializeRestClient(); + OpenSearchClient javaClient = client.initializeJavaClient(); + + try { + registerJobDetails(restClient); + } catch (WarningFailureException e) { + // ignore + } catch (Exception e) { + if (e instanceof ResourceAlreadyExistsException || e.getCause() instanceof ResourceAlreadyExistsException || e.getMessage().contains("resource_already_exists_exception")) { + // ignore + } else { + // Catch all other OpenSearchExceptions + return new ExtensionRestResponse(request, RestStatus.INTERNAL_SERVER_ERROR, e.getMessage()); + } + } + + try { + String mappingsJson = getHelloWorldJobMappings(); + GetMappingResponse response = fromJson(mappingsJson, GetMappingResponse.class); + TypeMapping mappings = response.get(GreetJob.HELLO_WORLD_JOB_INDEX).mappings(); + + CreateIndexRequest cir = new CreateIndexRequest.Builder() + .index(GreetJob.HELLO_WORLD_JOB_INDEX) + .mappings(mappings).build(); + + OpenSearchIndicesClient indicesClient = javaClient.indices(); + indicesClient.create(cir); + } catch (IOException e) { + return new ExtensionRestResponse(request, RestStatus.INTERNAL_SERVER_ERROR, e.getMessage()); + } catch (WarningFailureException e) { + // TODO This is failing on ConvertResponse. Ignoring. + /* + * org.opensearch.transport.RemoteTransportException: [hello-world][127.0.0.1:4532][internal:extensions/restexecuteonextensiontaction] + * Caused by: org.opensearch.common.io.stream.NotSerializableExceptionWrapper: warning_failure_exception: method [PUT], host [https://127.0.0.1:9200], URI [/.hello-world-jobs], status line [HTTP/2.0 200 OK] + * Warnings: [index name [.hello-world-jobs] starts with a dot '.', in the next major version, index names starting with a dot are reserved for hidden indices and system indices, this request accesses system indices: [.opendistro_security], but in a future major version, direct access to system indices will be prevented by default] + * {"acknowledged":true,"shards_acknowledged":true,"index":".hello-world-jobs"} + */ + } catch (OpenSearchException e) { + if (e instanceof ResourceAlreadyExistsException || e.getCause() instanceof ResourceAlreadyExistsException) { + } else { + // Catch all other OpenSearchExceptions + return new ExtensionRestResponse(request, RestStatus.INTERNAL_SERVER_ERROR, e.getMessage()); + } + } catch (Exception e) { + if (e.getMessage().contains("resource_already_exists_exception")) { + } else { + // Catch all other OpenSearchExceptions + return new ExtensionRestResponse(request, RestStatus.INTERNAL_SERVER_ERROR, e.getMessage()); + } + } + + Schedule schedule = new IntervalSchedule(Instant.now(), 1, ChronoUnit.MINUTES); + Duration duration = Duration.of(1, ChronoUnit.MINUTES); + + GreetJob job = new GreetJob( + "hw", + schedule, + true, + Instant.now(), + null, + Instant.now(), + duration.getSeconds() + ); + + try { + // Reference: AnomalyDetector - IndexAnomalyDetectorJobActionHandler.indexAnomalyDetectorJob + IndexRequest ir = new IndexRequest.Builder() + .index(GreetJob.HELLO_WORLD_JOB_INDEX) + .document(job.toPojo()) + .build(); + javaClient.index(ir); + } catch (IOException e) { + return new ExtensionRestResponse(request, RestStatus.INTERNAL_SERVER_ERROR, e.getMessage()); + } + + return new ExtensionRestResponse(request, OK, "GreetJob successfully scheduled"); + }; + private Function handleRemoteGetRequest = (request) -> { SDKClient client = extensionsRunner.getSdkClient(); @@ -70,14 +251,14 @@ public List routeHandlers() { // https://github.com/opensearch-project/opensearch-sdk-java/issues/584 CompletableFuture futureResponse = new CompletableFuture<>(); client.execute( - RemoteExtensionAction.INSTANCE, - proxyActionRequest, - ActionListener.wrap(r -> futureResponse.complete(r), e -> futureResponse.completeExceptionally(e)) + RemoteExtensionAction.INSTANCE, + proxyActionRequest, + ActionListener.wrap(r -> futureResponse.complete(r), e -> futureResponse.completeExceptionally(e)) ); try { RemoteExtensionActionResponse response = futureResponse.orTimeout( - ExtensionsManager.EXTENSION_REQUEST_WAIT_TIMEOUT, - TimeUnit.SECONDS + ExtensionsManager.EXTENSION_REQUEST_WAIT_TIMEOUT, + TimeUnit.SECONDS ).get(); if (!response.isSuccess()) { return new ExtensionRestResponse(request, OK, "Remote extension reponse failed: " + response.getResponseBytesAsString()); @@ -90,4 +271,4 @@ public List routeHandlers() { } }; -} +} \ No newline at end of file diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/schedule/GreetJob.java b/src/main/java/org/opensearch/sdk/sample/helloworld/schedule/GreetJob.java new file mode 100644 index 00000000..d419e15a --- /dev/null +++ b/src/main/java/org/opensearch/sdk/sample/helloworld/schedule/GreetJob.java @@ -0,0 +1,306 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.sdk.sample.helloworld.schedule; + +import com.google.common.base.Objects; +import org.opensearch.common.io.stream.StreamInput; +import org.opensearch.common.io.stream.StreamOutput; +import org.opensearch.common.io.stream.Writeable; +import org.opensearch.core.ParseField; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.core.xcontent.ToXContentObject; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.jobscheduler.spi.ScheduledJobParameter; +import org.opensearch.jobscheduler.spi.schedule.CronSchedule; +import org.opensearch.jobscheduler.spi.schedule.IntervalSchedule; +import org.opensearch.jobscheduler.spi.schedule.Schedule; +import org.opensearch.jobscheduler.spi.schedule.ScheduleParser; + +import java.io.IOException; +import java.time.Instant; + +import static org.opensearch.common.xcontent.XContentParserUtils.ensureExpectedToken; + +public class GreetJob implements Writeable, ToXContentObject, ScheduledJobParameter { + enum ScheduleType { + CRON, + INTERVAL + } + + public static final String PARSE_FIELD_NAME = "GreetJob"; + public static final NamedXContentRegistry.Entry XCONTENT_REGISTRY = new NamedXContentRegistry.Entry( + GreetJob.class, + new ParseField(PARSE_FIELD_NAME), + it -> parse(it) + ); + + public static final String HELLO_WORLD_JOB_INDEX = ".hello-world-jobs"; + public static final String NAME_FIELD = "name"; + public static final String LAST_UPDATE_TIME_FIELD = "last_update_time"; + public static final String LOCK_DURATION_SECONDS = "lock_duration_seconds"; + + public static final String SCHEDULE_FIELD = "schedule"; + public static final String IS_ENABLED_FIELD = "enabled"; + public static final String ENABLED_TIME_FIELD = "enabled_time"; + public static final String DISABLED_TIME_FIELD = "disabled_time"; + + private final String name; + private final Schedule schedule; + private final Boolean isEnabled; + private final Instant enabledTime; + private final Instant disabledTime; + private final Instant lastUpdateTime; + private final Long lockDurationSeconds; + + public GreetJob( + String name, + Schedule schedule, + Boolean isEnabled, + Instant enabledTime, + Instant disabledTime, + Instant lastUpdateTime, + Long lockDurationSeconds + ) { + this.name = name; + this.schedule = schedule; + this.isEnabled = isEnabled; + this.enabledTime = enabledTime; + this.disabledTime = disabledTime; + this.lastUpdateTime = lastUpdateTime; + this.lockDurationSeconds = lockDurationSeconds; + } + + public GreetJob(StreamInput input) throws IOException { + name = input.readString(); + if (input.readEnum(GreetJob.ScheduleType.class) == ScheduleType.CRON) { + schedule = new CronSchedule(input); + } else { + schedule = new IntervalSchedule(input); + } + isEnabled = input.readBoolean(); + enabledTime = input.readInstant(); + disabledTime = input.readInstant(); + lastUpdateTime = input.readInstant(); + lockDurationSeconds = input.readLong(); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException { + XContentBuilder xContentBuilder = builder.startObject() + .field(NAME_FIELD, name) + .field(SCHEDULE_FIELD, schedule) + .field(IS_ENABLED_FIELD, isEnabled) + .field(ENABLED_TIME_FIELD, enabledTime.toEpochMilli()) + .field(LAST_UPDATE_TIME_FIELD, lastUpdateTime.toEpochMilli()) + .field(LOCK_DURATION_SECONDS, lockDurationSeconds); + if (disabledTime != null) { + xContentBuilder.field(DISABLED_TIME_FIELD, disabledTime.toEpochMilli()); + } + return xContentBuilder.endObject(); + } + + @Override + public void writeTo(StreamOutput output) throws IOException { + output.writeString(name); + if (schedule instanceof CronSchedule) { + output.writeEnum(ScheduleType.CRON); + } else { + output.writeEnum(ScheduleType.INTERVAL); + } + schedule.writeTo(output); + output.writeBoolean(isEnabled); + output.writeInstant(enabledTime); + output.writeInstant(disabledTime); + output.writeInstant(lastUpdateTime); + output.writeLong(lockDurationSeconds); + } + + public static GreetJob parse(XContentParser parser) throws IOException { + String name = null; + Schedule schedule = null; + // we cannot set it to null as isEnabled() would do the unboxing and results in null pointer exception + Boolean isEnabled = Boolean.FALSE; + Instant enabledTime = null; + Instant disabledTime = null; + Instant lastUpdateTime = null; + Long lockDurationSeconds = 5L; + + ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.currentToken(), parser); + while (parser.nextToken() != XContentParser.Token.END_OBJECT) { + String fieldName = parser.currentName(); + parser.nextToken(); + + switch (fieldName) { + case NAME_FIELD: + name = parser.text(); + break; + case SCHEDULE_FIELD: + schedule = ScheduleParser.parse(parser); + break; + case IS_ENABLED_FIELD: + isEnabled = parser.booleanValue(); + break; + case ENABLED_TIME_FIELD: + enabledTime = toInstant(parser); + break; + case DISABLED_TIME_FIELD: + disabledTime = toInstant(parser); + break; + case LAST_UPDATE_TIME_FIELD: + lastUpdateTime = toInstant(parser); + break; + case LOCK_DURATION_SECONDS: + lockDurationSeconds = parser.longValue(); + break; + default: + parser.skipChildren(); + break; + } + } + return new GreetJob(name, schedule, isEnabled, enabledTime, disabledTime, lastUpdateTime, lockDurationSeconds); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + GreetJob that = (GreetJob) o; + return Objects.equal(getName(), that.getName()) + && Objects.equal(getSchedule(), that.getSchedule()) + && Objects.equal(isEnabled(), that.isEnabled()) + && Objects.equal(getEnabledTime(), that.getEnabledTime()) + && Objects.equal(getDisabledTime(), that.getDisabledTime()) + && Objects.equal(getLastUpdateTime(), that.getLastUpdateTime()) + && Objects.equal(getLockDurationSeconds(), that.getLockDurationSeconds()); + } + + @Override + public int hashCode() { + return Objects.hashCode(name, schedule, isEnabled, enabledTime, lastUpdateTime); + } + + @Override + public String getName() { + return name; + } + + @Override + public Schedule getSchedule() { + return schedule; + } + + @Override + public boolean isEnabled() { + return isEnabled; + } + + @Override + public Instant getEnabledTime() { + return enabledTime; + } + + public Instant getDisabledTime() { + return disabledTime; + } + + @Override + public Instant getLastUpdateTime() { + return lastUpdateTime; + } + + @Override + public Long getLockDurationSeconds() { + return lockDurationSeconds; + } + + /** + * Parse content parser to {@link java.time.Instant}. + * + * @param parser json based content parser + * @return instance of {@link java.time.Instant} + * @throws IOException IOException if content can't be parsed correctly + */ + public static Instant toInstant(XContentParser parser) throws IOException { + if (parser.currentToken() == null || parser.currentToken() == XContentParser.Token.VALUE_NULL) { + return null; + } + if (parser.currentToken().isValue()) { + return Instant.ofEpochMilli(parser.longValue()); + } + return null; + } + + public GreetJobPojo toPojo() { + GreetJobPojo.SchedulePojo.IntervalPojo interval = null; + if (this.schedule instanceof IntervalSchedule) { + interval = new GreetJobPojo.SchedulePojo.IntervalPojo( + ((IntervalSchedule)this.schedule).getUnit().toString(), + ((IntervalSchedule)this.schedule).getInterval(), + ((IntervalSchedule)this.schedule).getStartTime().toEpochMilli() + ); + } + return new GreetJobPojo( + this.enabledTime.toEpochMilli(), + this.lastUpdateTime.toEpochMilli(), + this.name, + this.lockDurationSeconds.intValue(), + this.isEnabled.booleanValue(), + new GreetJobPojo.SchedulePojo( + interval + )); + } + + public static class GreetJobPojo { + public long enabled_time; + public long last_update_time; + + public String name; + + public int lock_duration_seconds; + + public boolean enabled; + + public SchedulePojo schedule; + + public GreetJobPojo(long enabledTime, long lastUpdateTime, String name, int lockDurationSeconds, boolean enabled, SchedulePojo schedule) { + this.enabled_time = enabledTime; + this.last_update_time = lastUpdateTime; + this.name = name; + this.lock_duration_seconds = lockDurationSeconds; + this.enabled = enabled; + this.schedule = schedule; + } + + public static class SchedulePojo { + + public IntervalPojo interval; + + public SchedulePojo(IntervalPojo interval) { + this.interval = interval; + } + + public static class IntervalPojo { + public String unit; + + public int period; + + public long start_time; + + public IntervalPojo(String unit, int period, long start_time) { + this.unit = unit; + this.period = period; + this.start_time = start_time; + } + } + } + } +} \ No newline at end of file diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobParameterAction.java b/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobParameterAction.java new file mode 100644 index 00000000..bf7393a1 --- /dev/null +++ b/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobParameterAction.java @@ -0,0 +1,22 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.sdk.sample.helloworld.transport; + +import org.opensearch.action.ActionType; +import org.opensearch.jobscheduler.transport.response.JobParameterResponse; + +public class HWJobParameterAction extends ActionType { + public static final String NAME = "extensions:hw/greet_job_parameter"; + public static final HWJobParameterAction INSTANCE = new HWJobParameterAction(); + + private HWJobParameterAction() { + super(NAME, JobParameterResponse::new); + } +} \ No newline at end of file diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobParameterTransportAction.java b/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobParameterTransportAction.java new file mode 100644 index 00000000..5c824311 --- /dev/null +++ b/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobParameterTransportAction.java @@ -0,0 +1,72 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.sdk.sample.helloworld.transport; + +import com.google.inject.Inject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.action.ActionListener; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.TransportAction; +import org.opensearch.common.inject.Provides; +import org.opensearch.common.xcontent.LoggingDeprecationHandler; +import org.opensearch.common.xcontent.XContentHelper; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.jobscheduler.model.ExtensionJobParameter; +import org.opensearch.jobscheduler.spi.ScheduledJobParameter; +import org.opensearch.jobscheduler.transport.request.JobParameterRequest; +import org.opensearch.jobscheduler.transport.response.JobParameterResponse; +import org.opensearch.sdk.SDKNamedXContentRegistry; +import org.opensearch.sdk.sample.helloworld.schedule.GreetJob; +import org.opensearch.tasks.Task; +import org.opensearch.tasks.TaskManager; + +import static org.opensearch.common.xcontent.XContentParserUtils.ensureExpectedToken; +import static org.opensearch.sdk.sample.helloworld.util.RestHandlerUtils.wrapRestActionListener; + +public class HWJobParameterTransportAction extends TransportAction { + + private static final Logger LOG = LogManager.getLogger(HWJobParameterTransportAction.class); + + private final SDKNamedXContentRegistry xContentRegistry; + + @Inject + protected HWJobParameterTransportAction( + ActionFilters actionFilters, + TaskManager taskManager, + SDKNamedXContentRegistry xContentRegistry + ) { + super(HWJobParameterAction.NAME, actionFilters, taskManager); + this.xContentRegistry = xContentRegistry; + } + + @Override + protected void doExecute(Task task, JobParameterRequest request, ActionListener actionListener) { + + String errorMessage = "Failed to parse the Job Parameter"; + ActionListener listener = wrapRestActionListener(actionListener, errorMessage); + try { + XContentParser parser = XContentHelper.createParser( + xContentRegistry.getRegistry(), + LoggingDeprecationHandler.INSTANCE, + request.getJobSource(), + XContentType.JSON + ); + ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser); + ScheduledJobParameter scheduledJobParameter = GreetJob.parse(parser); + JobParameterResponse jobParameterResponse = new JobParameterResponse(new ExtensionJobParameter(scheduledJobParameter)); + listener.onResponse(jobParameterResponse); + } catch (Exception e) { + LOG.error(e); + listener.onFailure(e); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobRunnerAction.java b/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobRunnerAction.java new file mode 100644 index 00000000..a172478e --- /dev/null +++ b/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobRunnerAction.java @@ -0,0 +1,23 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.sdk.sample.helloworld.transport; + +import org.opensearch.action.ActionType; +import org.opensearch.jobscheduler.transport.response.JobRunnerResponse; + +public class HWJobRunnerAction extends ActionType { + + public static final String NAME = "extensions:hw/greet_job_runner"; + public static final HWJobRunnerAction INSTANCE = new HWJobRunnerAction(); + + private HWJobRunnerAction() { + super(NAME, JobRunnerResponse::new); + } +} \ No newline at end of file diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobRunnerTransportAction.java b/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobRunnerTransportAction.java new file mode 100644 index 00000000..7a488809 --- /dev/null +++ b/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobRunnerTransportAction.java @@ -0,0 +1,154 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.sdk.sample.helloworld.transport; + +import com.google.inject.Inject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.action.ActionListener; +import org.opensearch.action.get.GetRequest; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.TransportAction; +import org.opensearch.common.inject.Provides; +import org.opensearch.common.xcontent.LoggingDeprecationHandler; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.jobscheduler.spi.JobExecutionContext; +import org.opensearch.jobscheduler.transport.request.JobRunnerRequest; +import org.opensearch.jobscheduler.transport.response.JobRunnerResponse; +import org.opensearch.sdk.SDKClient.SDKRestClient; +import org.opensearch.sdk.SDKNamedXContentRegistry; +import org.opensearch.sdk.sample.helloworld.schedule.GreetJob; +import org.opensearch.tasks.Task; +import org.opensearch.tasks.TaskManager; + +import java.io.IOException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static org.opensearch.common.xcontent.XContentParserUtils.ensureExpectedToken; +import static org.opensearch.sdk.sample.helloworld.util.RestHandlerUtils.wrapRestActionListener; + +public class HWJobRunnerTransportAction extends TransportAction { + + private static final Logger LOG = LogManager.getLogger(HWJobRunnerTransportAction.class); + + private SDKRestClient client; + private final SDKNamedXContentRegistry xContentRegistry; + + @Inject + protected HWJobRunnerTransportAction( + ActionFilters actionFilters, + TaskManager taskManager, + SDKNamedXContentRegistry xContentRegistry, + SDKRestClient client + ) { + super(HWJobRunnerAction.NAME, actionFilters, taskManager); + this.client = client; + this.xContentRegistry = xContentRegistry; + } + + @Override + protected void doExecute(Task task, JobRunnerRequest request, ActionListener actionListener) { + String errorMessage = "Failed to run the Job"; + ActionListener listener = wrapRestActionListener(actionListener, errorMessage); + try { + JobExecutionContext jobExecutionContext = request.getJobExecutionContext(); + String jobParameterDocumentId = jobExecutionContext.getJobId(); + if (jobParameterDocumentId == null || jobParameterDocumentId.isEmpty()) { + listener.onFailure(new IllegalArgumentException("jobParameterDocumentId cannot be empty or null")); + } else { + CompletableFuture inProgressFuture = new CompletableFuture<>(); + findById(jobParameterDocumentId, new ActionListener<>() { + @Override + public void onResponse(GreetJob anomalyDetectorJob) { + inProgressFuture.complete(anomalyDetectorJob); + } + + @Override + public void onFailure(Exception e) { + logger.info("could not find GreetJob with id " + jobParameterDocumentId, e); + inProgressFuture.completeExceptionally(e); + } + }); + + try { + GreetJob scheduledJobParameter = inProgressFuture.orTimeout(10000, TimeUnit.MILLISECONDS).join(); + + JobRunnerResponse jobRunnerResponse; + if (scheduledJobParameter != null && validateJobExecutionContext(jobExecutionContext)) { + jobRunnerResponse = new JobRunnerResponse(true); + } else { + jobRunnerResponse = new JobRunnerResponse(false); + } + listener.onResponse(jobRunnerResponse); + if (jobRunnerResponse.getJobRunnerStatus()) { + HelloWorldJobRunner.getJobRunnerInstance().runJob(scheduledJobParameter, jobExecutionContext); + } + } catch (CompletionException e) { + if (e.getCause() instanceof TimeoutException) { + logger.info(" Request timed out with an exception ", e); + } else { + throw e; + } + } catch (Exception e) { + logger.info(" Could not find Job Parameter due to exception ", e); + } + + } + } catch (Exception e) { + LOG.error(e); + listener.onFailure(e); + } + } + + private void findById(String jobParameterId, ActionListener listener) { + GetRequest getRequest = new GetRequest(GreetJob.HELLO_WORLD_JOB_INDEX, jobParameterId); + try { + client.get(getRequest, ActionListener.wrap(response -> { + if (!response.isExists()) { + listener.onResponse(null); + } else { + try { + XContentParser parser = XContentType.JSON.xContent() + .createParser(xContentRegistry.getRegistry(), LoggingDeprecationHandler.INSTANCE, response.getSourceAsString()); + ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser); + listener.onResponse(GreetJob.parse(parser)); + } catch (IOException e) { + logger.error("IOException occurred finding GreetJob for jobParameterId " + jobParameterId, e); + listener.onFailure(e); + } + } + }, exception -> { + logger.error("Exception occurred finding GreetJob for jobParameterId " + jobParameterId, exception); + listener.onFailure(exception); + })); + } catch (Exception e) { + logger.error("Error occurred finding greet job with jobParameterId " + jobParameterId, e); + listener.onFailure(e); + } + + } + + private boolean validateJobExecutionContext(JobExecutionContext jobExecutionContext) { + if (jobExecutionContext != null + && jobExecutionContext.getJobId() != null + && !jobExecutionContext.getJobId().isEmpty() + && jobExecutionContext.getJobIndexName() != null + && !jobExecutionContext.getJobIndexName().isEmpty() + && jobExecutionContext.getExpectedExecutionTime() != null + && jobExecutionContext.getJobVersion() != null) { + return true; + } + return false; + } +} \ No newline at end of file diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HelloWorldJobRunner.java b/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HelloWorldJobRunner.java new file mode 100644 index 00000000..7623b56c --- /dev/null +++ b/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HelloWorldJobRunner.java @@ -0,0 +1,40 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.sdk.sample.helloworld.transport; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.jobscheduler.spi.JobExecutionContext; +import org.opensearch.jobscheduler.spi.ScheduledJobParameter; +import org.opensearch.jobscheduler.spi.ScheduledJobRunner; + +public class HelloWorldJobRunner implements ScheduledJobRunner { + private static final Logger log = LogManager.getLogger(HelloWorldJobRunner.class); + + private static HelloWorldJobRunner INSTANCE; + + public static HelloWorldJobRunner getJobRunnerInstance() { + if (INSTANCE != null) { + return INSTANCE; + } + synchronized (HelloWorldJobRunner.class) { + if (INSTANCE != null) { + return INSTANCE; + } + INSTANCE = new HelloWorldJobRunner(); + return INSTANCE; + } + } + + @Override + public void runJob(ScheduledJobParameter job, JobExecutionContext context) { + System.out.println("Hello, world!"); + } +} \ No newline at end of file diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/util/RestHandlerUtils.java b/src/main/java/org/opensearch/sdk/sample/helloworld/util/RestHandlerUtils.java new file mode 100644 index 00000000..224f5656 --- /dev/null +++ b/src/main/java/org/opensearch/sdk/sample/helloworld/util/RestHandlerUtils.java @@ -0,0 +1,83 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.sdk.sample.helloworld.util; + +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableMap; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.OpenSearchStatusException; +import org.opensearch.action.ActionListener; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.index.IndexNotFoundException; +import org.opensearch.indices.InvalidIndexNameException; +import org.opensearch.rest.RestStatus; + +import static org.opensearch.rest.RestStatus.BAD_REQUEST; +import static org.opensearch.rest.RestStatus.INTERNAL_SERVER_ERROR; + +/** + * Utility functions for REST handlers. + */ +public final class RestHandlerUtils { + private static final Logger logger = LogManager.getLogger(RestHandlerUtils.class); + + public static final ToXContent.MapParams XCONTENT_WITH_TYPE = new ToXContent.MapParams(ImmutableMap.of("with_type", "true")); + + private RestHandlerUtils() {} + + /** + * Wrap action listener to avoid return verbose error message and wrong 500 error to user. + * Suggestion for exception handling in HW: + * 1. If the error is caused by wrong input, throw IllegalArgumentException exception. + * 2. For other errors, please use OpenSearchStatusException. + * + * TODO: tune this function for wrapped exception, return root exception error message + * + * @param actionListener action listener + * @param generalErrorMessage general error message + * @param action listener response type + * @return wrapped action listener + */ + public static ActionListener wrapRestActionListener(ActionListener actionListener, String generalErrorMessage) { + return ActionListener.wrap(r -> { actionListener.onResponse(r); }, e -> { + logger.error("Wrap exception before sending back to user", e); + Throwable cause = Throwables.getRootCause(e); + if (isProperExceptionToReturn(e)) { + actionListener.onFailure(e); + } else if (isProperExceptionToReturn(cause)) { + actionListener.onFailure((Exception) cause); + } else { + RestStatus status = isBadRequest(e) ? BAD_REQUEST : INTERNAL_SERVER_ERROR; + String errorMessage = generalErrorMessage; + if (isBadRequest(e)) { + errorMessage = e.getMessage(); + } else if (cause != null && isBadRequest(cause)) { + errorMessage = cause.getMessage(); + } + actionListener.onFailure(new OpenSearchStatusException(errorMessage, status)); + } + }); + } + + public static boolean isBadRequest(Throwable e) { + if (e == null) { + return false; + } + return e instanceof IllegalArgumentException; + } + + public static boolean isProperExceptionToReturn(Throwable e) { + if (e == null) { + return false; + } + return e instanceof OpenSearchStatusException || e instanceof IndexNotFoundException || e instanceof InvalidIndexNameException; + } +} \ No newline at end of file diff --git a/src/main/resources/mappings/hello-world-jobs.json b/src/main/resources/mappings/hello-world-jobs.json new file mode 100644 index 00000000..43cfec8c --- /dev/null +++ b/src/main/resources/mappings/hello-world-jobs.json @@ -0,0 +1,50 @@ +{ + ".hello-world-jobs": { + "mappings": { + "properties": { + "schema_version": { + "type": "integer" + }, + "name": { + "type": "keyword" + }, + "schedule": { + "properties": { + "interval": { + "properties": { + "start_time": { + "type": "date", + "format": "strict_date_time||epoch_millis" + }, + "period": { + "type": "integer" + }, + "unit": { + "type": "keyword" + } + } + } + } + }, + "enabled": { + "type": "boolean" + }, + "enabled_time": { + "type": "date", + "format": "strict_date_time||epoch_millis" + }, + "disabled_time": { + "type": "date", + "format": "strict_date_time||epoch_millis" + }, + "last_update_time": { + "type": "date", + "format": "strict_date_time||epoch_millis" + }, + "lock_duration_seconds": { + "type": "long" + } + } + } + } +} From 0afb4ba2715e8d975d669bc264f4896d1290b219 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Sat, 22 Apr 2023 21:25:18 -0400 Subject: [PATCH 02/16] Run spotlessApply Signed-off-by: Craig Perkins --- build.gradle | 2 +- .../helloworld/HelloWorldExtension.java | 4 +- .../rest/RestRemoteHelloAction.java | 52 ++++--------- .../sample/helloworld/schedule/GreetJob.java | 76 ++++++++++--------- .../transport/HWJobParameterAction.java | 2 +- .../HWJobParameterTransportAction.java | 17 ++--- .../transport/HWJobRunnerAction.java | 2 +- .../transport/HWJobRunnerTransportAction.java | 25 +++--- .../transport/HelloWorldJobRunner.java | 2 +- .../helloworld/util/RestHandlerUtils.java | 2 +- 10 files changed, 83 insertions(+), 101 deletions(-) diff --git a/build.gradle b/build.gradle index 8aaa8169..639be92e 100644 --- a/build.gradle +++ b/build.gradle @@ -92,7 +92,7 @@ dependencies { // job-scheduler dependency is added for hello world sample extension implementation("org.opensearch.plugin:opensearch-job-scheduler:${jobSchedulerVersion}") implementation "org.opensearch:opensearch-job-scheduler-spi:${jobSchedulerVersion}" - + implementation("org.apache.logging.log4j:log4j-api:${log4jVersion}") implementation("org.apache.logging.log4j:log4j-core:${log4jVersion}") implementation("org.apache.logging.log4j:log4j-jul:${log4jVersion}") diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/HelloWorldExtension.java b/src/main/java/org/opensearch/sdk/sample/helloworld/HelloWorldExtension.java index 44d4604a..c4fc8ba3 100644 --- a/src/main/java/org/opensearch/sdk/sample/helloworld/HelloWorldExtension.java +++ b/src/main/java/org/opensearch/sdk/sample/helloworld/HelloWorldExtension.java @@ -80,9 +80,7 @@ public List getExtensionRestHandlers() { @Override public List getNamedXContent() { - return ImmutableList.of( - GreetJob.XCONTENT_REGISTRY - ); + return ImmutableList.of(GreetJob.XCONTENT_REGISTRY); } @Deprecated diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/rest/RestRemoteHelloAction.java b/src/main/java/org/opensearch/sdk/sample/helloworld/rest/RestRemoteHelloAction.java index a0a29e7f..ffd0c245 100644 --- a/src/main/java/org/opensearch/sdk/sample/helloworld/rest/RestRemoteHelloAction.java +++ b/src/main/java/org/opensearch/sdk/sample/helloworld/rest/RestRemoteHelloAction.java @@ -9,7 +9,6 @@ package org.opensearch.sdk.sample.helloworld.rest; -import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.io.Resources; import jakarta.json.stream.JsonParser; import org.apache.commons.codec.Charsets; @@ -29,11 +28,7 @@ import org.opensearch.client.opensearch.indices.GetMappingResponse; import org.opensearch.client.opensearch.indices.OpenSearchIndicesClient; import org.opensearch.common.Strings; -import org.opensearch.common.bytes.BytesReference; import org.opensearch.common.io.stream.StreamInput; -import org.opensearch.common.xcontent.XContentFactory; -import org.opensearch.common.xcontent.XContentHelper; -import org.opensearch.common.xcontent.XContentType; import org.opensearch.common.xcontent.json.JsonXContent; import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.extensions.ExtensionsManager; @@ -56,7 +51,6 @@ import org.opensearch.sdk.sample.helloworld.transport.SampleAction; import org.opensearch.sdk.sample.helloworld.transport.SampleRequest; import org.opensearch.sdk.sample.helloworld.transport.SampleResponse; -import org.opensearch.sdk.sample.helloworld.util.RestHandlerUtils; import java.io.IOException; import java.io.StringReader; @@ -66,7 +60,6 @@ import java.time.temporal.ChronoUnit; import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.Random; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; @@ -94,8 +87,8 @@ public RestRemoteHelloAction(ExtensionsRunner runner) { @Override public List routeHandlers() { return List.of( - new RouteHandler(GET, "/hello/{name}", handleRemoteGetRequest), - new RouteHandler(PUT, "/schedule/hello", handleScheduleRequest) + new RouteHandler(GET, "/hello/{name}", handleRemoteGetRequest), + new RouteHandler(PUT, "/schedule/hello", handleScheduleRequest) ); } @@ -164,7 +157,9 @@ public T fromJson(String json, Class clazz) { } catch (WarningFailureException e) { // ignore } catch (Exception e) { - if (e instanceof ResourceAlreadyExistsException || e.getCause() instanceof ResourceAlreadyExistsException || e.getMessage().contains("resource_already_exists_exception")) { + if (e instanceof ResourceAlreadyExistsException + || e.getCause() instanceof ResourceAlreadyExistsException + || e.getMessage().contains("resource_already_exists_exception")) { // ignore } else { // Catch all other OpenSearchExceptions @@ -177,9 +172,7 @@ public T fromJson(String json, Class clazz) { GetMappingResponse response = fromJson(mappingsJson, GetMappingResponse.class); TypeMapping mappings = response.get(GreetJob.HELLO_WORLD_JOB_INDEX).mappings(); - CreateIndexRequest cir = new CreateIndexRequest.Builder() - .index(GreetJob.HELLO_WORLD_JOB_INDEX) - .mappings(mappings).build(); + CreateIndexRequest cir = new CreateIndexRequest.Builder().index(GreetJob.HELLO_WORLD_JOB_INDEX).mappings(mappings).build(); OpenSearchIndicesClient indicesClient = javaClient.indices(); indicesClient.create(cir); @@ -194,14 +187,12 @@ public T fromJson(String json, Class clazz) { * {"acknowledged":true,"shards_acknowledged":true,"index":".hello-world-jobs"} */ } catch (OpenSearchException e) { - if (e instanceof ResourceAlreadyExistsException || e.getCause() instanceof ResourceAlreadyExistsException) { - } else { + if (e instanceof ResourceAlreadyExistsException || e.getCause() instanceof ResourceAlreadyExistsException) {} else { // Catch all other OpenSearchExceptions return new ExtensionRestResponse(request, RestStatus.INTERNAL_SERVER_ERROR, e.getMessage()); } } catch (Exception e) { - if (e.getMessage().contains("resource_already_exists_exception")) { - } else { + if (e.getMessage().contains("resource_already_exists_exception")) {} else { // Catch all other OpenSearchExceptions return new ExtensionRestResponse(request, RestStatus.INTERNAL_SERVER_ERROR, e.getMessage()); } @@ -210,22 +201,11 @@ public T fromJson(String json, Class clazz) { Schedule schedule = new IntervalSchedule(Instant.now(), 1, ChronoUnit.MINUTES); Duration duration = Duration.of(1, ChronoUnit.MINUTES); - GreetJob job = new GreetJob( - "hw", - schedule, - true, - Instant.now(), - null, - Instant.now(), - duration.getSeconds() - ); + GreetJob job = new GreetJob("hw", schedule, true, Instant.now(), null, Instant.now(), duration.getSeconds()); try { // Reference: AnomalyDetector - IndexAnomalyDetectorJobActionHandler.indexAnomalyDetectorJob - IndexRequest ir = new IndexRequest.Builder() - .index(GreetJob.HELLO_WORLD_JOB_INDEX) - .document(job.toPojo()) - .build(); + IndexRequest ir = new IndexRequest.Builder().index(GreetJob.HELLO_WORLD_JOB_INDEX).document(job.toPojo()).build(); javaClient.index(ir); } catch (IOException e) { return new ExtensionRestResponse(request, RestStatus.INTERNAL_SERVER_ERROR, e.getMessage()); @@ -251,14 +231,14 @@ public T fromJson(String json, Class clazz) { // https://github.com/opensearch-project/opensearch-sdk-java/issues/584 CompletableFuture futureResponse = new CompletableFuture<>(); client.execute( - RemoteExtensionAction.INSTANCE, - proxyActionRequest, - ActionListener.wrap(r -> futureResponse.complete(r), e -> futureResponse.completeExceptionally(e)) + RemoteExtensionAction.INSTANCE, + proxyActionRequest, + ActionListener.wrap(r -> futureResponse.complete(r), e -> futureResponse.completeExceptionally(e)) ); try { RemoteExtensionActionResponse response = futureResponse.orTimeout( - ExtensionsManager.EXTENSION_REQUEST_WAIT_TIMEOUT, - TimeUnit.SECONDS + ExtensionsManager.EXTENSION_REQUEST_WAIT_TIMEOUT, + TimeUnit.SECONDS ).get(); if (!response.isSuccess()) { return new ExtensionRestResponse(request, OK, "Remote extension reponse failed: " + response.getResponseBytesAsString()); @@ -271,4 +251,4 @@ public T fromJson(String json, Class clazz) { } }; -} \ No newline at end of file +} diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/schedule/GreetJob.java b/src/main/java/org/opensearch/sdk/sample/helloworld/schedule/GreetJob.java index d419e15a..e69b5edb 100644 --- a/src/main/java/org/opensearch/sdk/sample/helloworld/schedule/GreetJob.java +++ b/src/main/java/org/opensearch/sdk/sample/helloworld/schedule/GreetJob.java @@ -38,9 +38,9 @@ enum ScheduleType { public static final String PARSE_FIELD_NAME = "GreetJob"; public static final NamedXContentRegistry.Entry XCONTENT_REGISTRY = new NamedXContentRegistry.Entry( - GreetJob.class, - new ParseField(PARSE_FIELD_NAME), - it -> parse(it) + GreetJob.class, + new ParseField(PARSE_FIELD_NAME), + it -> parse(it) ); public static final String HELLO_WORLD_JOB_INDEX = ".hello-world-jobs"; @@ -62,13 +62,13 @@ enum ScheduleType { private final Long lockDurationSeconds; public GreetJob( - String name, - Schedule schedule, - Boolean isEnabled, - Instant enabledTime, - Instant disabledTime, - Instant lastUpdateTime, - Long lockDurationSeconds + String name, + Schedule schedule, + Boolean isEnabled, + Instant enabledTime, + Instant disabledTime, + Instant lastUpdateTime, + Long lockDurationSeconds ) { this.name = name; this.schedule = schedule; @@ -96,12 +96,12 @@ public GreetJob(StreamInput input) throws IOException { @Override public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException { XContentBuilder xContentBuilder = builder.startObject() - .field(NAME_FIELD, name) - .field(SCHEDULE_FIELD, schedule) - .field(IS_ENABLED_FIELD, isEnabled) - .field(ENABLED_TIME_FIELD, enabledTime.toEpochMilli()) - .field(LAST_UPDATE_TIME_FIELD, lastUpdateTime.toEpochMilli()) - .field(LOCK_DURATION_SECONDS, lockDurationSeconds); + .field(NAME_FIELD, name) + .field(SCHEDULE_FIELD, schedule) + .field(IS_ENABLED_FIELD, isEnabled) + .field(ENABLED_TIME_FIELD, enabledTime.toEpochMilli()) + .field(LAST_UPDATE_TIME_FIELD, lastUpdateTime.toEpochMilli()) + .field(LOCK_DURATION_SECONDS, lockDurationSeconds); if (disabledTime != null) { xContentBuilder.field(DISABLED_TIME_FIELD, disabledTime.toEpochMilli()); } @@ -175,12 +175,12 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; GreetJob that = (GreetJob) o; return Objects.equal(getName(), that.getName()) - && Objects.equal(getSchedule(), that.getSchedule()) - && Objects.equal(isEnabled(), that.isEnabled()) - && Objects.equal(getEnabledTime(), that.getEnabledTime()) - && Objects.equal(getDisabledTime(), that.getDisabledTime()) - && Objects.equal(getLastUpdateTime(), that.getLastUpdateTime()) - && Objects.equal(getLockDurationSeconds(), that.getLockDurationSeconds()); + && Objects.equal(getSchedule(), that.getSchedule()) + && Objects.equal(isEnabled(), that.isEnabled()) + && Objects.equal(getEnabledTime(), that.getEnabledTime()) + && Objects.equal(getDisabledTime(), that.getDisabledTime()) + && Objects.equal(getLastUpdateTime(), that.getLastUpdateTime()) + && Objects.equal(getLockDurationSeconds(), that.getLockDurationSeconds()); } @Override @@ -243,20 +243,19 @@ public GreetJobPojo toPojo() { GreetJobPojo.SchedulePojo.IntervalPojo interval = null; if (this.schedule instanceof IntervalSchedule) { interval = new GreetJobPojo.SchedulePojo.IntervalPojo( - ((IntervalSchedule)this.schedule).getUnit().toString(), - ((IntervalSchedule)this.schedule).getInterval(), - ((IntervalSchedule)this.schedule).getStartTime().toEpochMilli() + ((IntervalSchedule) this.schedule).getUnit().toString(), + ((IntervalSchedule) this.schedule).getInterval(), + ((IntervalSchedule) this.schedule).getStartTime().toEpochMilli() ); } return new GreetJobPojo( - this.enabledTime.toEpochMilli(), - this.lastUpdateTime.toEpochMilli(), - this.name, - this.lockDurationSeconds.intValue(), - this.isEnabled.booleanValue(), - new GreetJobPojo.SchedulePojo( - interval - )); + this.enabledTime.toEpochMilli(), + this.lastUpdateTime.toEpochMilli(), + this.name, + this.lockDurationSeconds.intValue(), + this.isEnabled.booleanValue(), + new GreetJobPojo.SchedulePojo(interval) + ); } public static class GreetJobPojo { @@ -271,7 +270,14 @@ public static class GreetJobPojo { public SchedulePojo schedule; - public GreetJobPojo(long enabledTime, long lastUpdateTime, String name, int lockDurationSeconds, boolean enabled, SchedulePojo schedule) { + public GreetJobPojo( + long enabledTime, + long lastUpdateTime, + String name, + int lockDurationSeconds, + boolean enabled, + SchedulePojo schedule + ) { this.enabled_time = enabledTime; this.last_update_time = lastUpdateTime; this.name = name; @@ -303,4 +309,4 @@ public IntervalPojo(String unit, int period, long start_time) { } } } -} \ No newline at end of file +} diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobParameterAction.java b/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobParameterAction.java index bf7393a1..0a8dd4ee 100644 --- a/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobParameterAction.java +++ b/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobParameterAction.java @@ -19,4 +19,4 @@ public class HWJobParameterAction extends ActionType { private HWJobParameterAction() { super(NAME, JobParameterResponse::new); } -} \ No newline at end of file +} diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobParameterTransportAction.java b/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobParameterTransportAction.java index 5c824311..e2619bc1 100644 --- a/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobParameterTransportAction.java +++ b/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobParameterTransportAction.java @@ -15,7 +15,6 @@ import org.opensearch.action.ActionListener; import org.opensearch.action.support.ActionFilters; import org.opensearch.action.support.TransportAction; -import org.opensearch.common.inject.Provides; import org.opensearch.common.xcontent.LoggingDeprecationHandler; import org.opensearch.common.xcontent.XContentHelper; import org.opensearch.common.xcontent.XContentType; @@ -40,9 +39,9 @@ public class HWJobParameterTransportAction extends TransportAction listener = wrapRestActionListener(actionListener, errorMessage); try { XContentParser parser = XContentHelper.createParser( - xContentRegistry.getRegistry(), - LoggingDeprecationHandler.INSTANCE, - request.getJobSource(), - XContentType.JSON + xContentRegistry.getRegistry(), + LoggingDeprecationHandler.INSTANCE, + request.getJobSource(), + XContentType.JSON ); ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser); ScheduledJobParameter scheduledJobParameter = GreetJob.parse(parser); @@ -69,4 +68,4 @@ protected void doExecute(Task task, JobParameterRequest request, ActionListener< listener.onFailure(e); } } -} \ No newline at end of file +} diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobRunnerAction.java b/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobRunnerAction.java index a172478e..4698dbe1 100644 --- a/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobRunnerAction.java +++ b/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobRunnerAction.java @@ -20,4 +20,4 @@ public class HWJobRunnerAction extends ActionType { private HWJobRunnerAction() { super(NAME, JobRunnerResponse::new); } -} \ No newline at end of file +} diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobRunnerTransportAction.java b/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobRunnerTransportAction.java index 7a488809..c3695b12 100644 --- a/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobRunnerTransportAction.java +++ b/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobRunnerTransportAction.java @@ -16,7 +16,6 @@ import org.opensearch.action.get.GetRequest; import org.opensearch.action.support.ActionFilters; import org.opensearch.action.support.TransportAction; -import org.opensearch.common.inject.Provides; import org.opensearch.common.xcontent.LoggingDeprecationHandler; import org.opensearch.common.xcontent.XContentType; import org.opensearch.core.xcontent.XContentParser; @@ -47,10 +46,10 @@ public class HWJobRunnerTransportAction extends TransportAction listener) } else { try { XContentParser parser = XContentType.JSON.xContent() - .createParser(xContentRegistry.getRegistry(), LoggingDeprecationHandler.INSTANCE, response.getSourceAsString()); + .createParser(xContentRegistry.getRegistry(), LoggingDeprecationHandler.INSTANCE, response.getSourceAsString()); ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser); listener.onResponse(GreetJob.parse(parser)); } catch (IOException e) { @@ -141,14 +140,14 @@ private void findById(String jobParameterId, ActionListener listener) private boolean validateJobExecutionContext(JobExecutionContext jobExecutionContext) { if (jobExecutionContext != null - && jobExecutionContext.getJobId() != null - && !jobExecutionContext.getJobId().isEmpty() - && jobExecutionContext.getJobIndexName() != null - && !jobExecutionContext.getJobIndexName().isEmpty() - && jobExecutionContext.getExpectedExecutionTime() != null - && jobExecutionContext.getJobVersion() != null) { + && jobExecutionContext.getJobId() != null + && !jobExecutionContext.getJobId().isEmpty() + && jobExecutionContext.getJobIndexName() != null + && !jobExecutionContext.getJobIndexName().isEmpty() + && jobExecutionContext.getExpectedExecutionTime() != null + && jobExecutionContext.getJobVersion() != null) { return true; } return false; } -} \ No newline at end of file +} diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HelloWorldJobRunner.java b/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HelloWorldJobRunner.java index 7623b56c..c3fa05dc 100644 --- a/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HelloWorldJobRunner.java +++ b/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HelloWorldJobRunner.java @@ -37,4 +37,4 @@ public static HelloWorldJobRunner getJobRunnerInstance() { public void runJob(ScheduledJobParameter job, JobExecutionContext context) { System.out.println("Hello, world!"); } -} \ No newline at end of file +} diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/util/RestHandlerUtils.java b/src/main/java/org/opensearch/sdk/sample/helloworld/util/RestHandlerUtils.java index 224f5656..0e2d3d1e 100644 --- a/src/main/java/org/opensearch/sdk/sample/helloworld/util/RestHandlerUtils.java +++ b/src/main/java/org/opensearch/sdk/sample/helloworld/util/RestHandlerUtils.java @@ -80,4 +80,4 @@ public static boolean isProperExceptionToReturn(Throwable e) { } return e instanceof OpenSearchStatusException || e instanceof IndexNotFoundException || e instanceof InvalidIndexNameException; } -} \ No newline at end of file +} From 14a35b995ec012cb6a5dc90558dab3fa0e97e1e2 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Mon, 24 Apr 2023 10:01:37 -0400 Subject: [PATCH 03/16] Re-run CI Signed-off-by: Craig Perkins --- build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/build.gradle b/build.gradle index 639be92e..4f01e509 100644 --- a/build.gradle +++ b/build.gradle @@ -88,7 +88,6 @@ dependencies { def jaxbVersion = "2.3.1" implementation("org.opensearch:opensearch:${opensearchVersion}") - // job-scheduler dependency is added for hello world sample extension implementation("org.opensearch.plugin:opensearch-job-scheduler:${jobSchedulerVersion}") implementation "org.opensearch:opensearch-job-scheduler-spi:${jobSchedulerVersion}" From a5c6111ce6ba3da4db10b898844f9b7c41df9f07 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Mon, 24 Apr 2023 10:07:11 -0400 Subject: [PATCH 04/16] Use different artifact id Signed-off-by: Craig Perkins --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 4f01e509..567eaca8 100644 --- a/build.gradle +++ b/build.gradle @@ -90,6 +90,7 @@ dependencies { implementation("org.opensearch:opensearch:${opensearchVersion}") // job-scheduler dependency is added for hello world sample extension implementation("org.opensearch.plugin:opensearch-job-scheduler:${jobSchedulerVersion}") + implementation("org.opensearch:opensearch-job-scheduler:${jobSchedulerVersion}") implementation "org.opensearch:opensearch-job-scheduler-spi:${jobSchedulerVersion}" implementation("org.apache.logging.log4j:log4j-api:${log4jVersion}") From 09fb3e6544aaa0827a317e0851e2d553105d4b78 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Mon, 24 Apr 2023 11:27:51 -0400 Subject: [PATCH 05/16] Add javadoc Signed-off-by: Craig Perkins --- .../rest/RestRemoteHelloAction.java | 9 +++ .../sample/helloworld/schedule/GreetJob.java | 68 +++++++++++++++++++ .../transport/HWJobParameterAction.java | 3 + .../HWJobParameterTransportAction.java | 10 +++ .../transport/HWJobRunnerAction.java | 3 + .../transport/HWJobRunnerTransportAction.java | 11 +++ .../transport/HelloWorldJobRunner.java | 7 ++ .../helloworld/util/RestHandlerUtils.java | 8 +++ 8 files changed, 119 insertions(+) diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/rest/RestRemoteHelloAction.java b/src/main/java/org/opensearch/sdk/sample/helloworld/rest/RestRemoteHelloAction.java index ffd0c245..be6e1220 100644 --- a/src/main/java/org/opensearch/sdk/sample/helloworld/rest/RestRemoteHelloAction.java +++ b/src/main/java/org/opensearch/sdk/sample/helloworld/rest/RestRemoteHelloAction.java @@ -140,6 +140,15 @@ public boolean ignoreUnknownFields() { } } + /** + * Deserializes json string into a java object + * + * @param json The JSON string + * @param clazz The class to deserialize to + * @param The class to deserialize to + * + * @return Object instance of clazz requested + */ public T fromJson(String json, Class clazz) { int rand = new Random().nextInt(); JsonpMapper mapper = setupMapper(rand); diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/schedule/GreetJob.java b/src/main/java/org/opensearch/sdk/sample/helloworld/schedule/GreetJob.java index e69b5edb..be0be3dd 100644 --- a/src/main/java/org/opensearch/sdk/sample/helloworld/schedule/GreetJob.java +++ b/src/main/java/org/opensearch/sdk/sample/helloworld/schedule/GreetJob.java @@ -30,6 +30,9 @@ import static org.opensearch.common.xcontent.XContentParserUtils.ensureExpectedToken; +/** + * Sample scheduled job for the HelloWorld Extension + */ public class GreetJob implements Writeable, ToXContentObject, ScheduledJobParameter { enum ScheduleType { CRON, @@ -61,6 +64,16 @@ enum ScheduleType { private final Instant lastUpdateTime; private final Long lockDurationSeconds; + /** + * + * @param name name of the scheduled job + * @param schedule The schedule, cron or interval, the job run will run with + * @param isEnabled Flag to indices whether this job is enabled + * @param enabledTime Timestamp when the job was last enabled + * @param disabledTime Timestamp when the job was last disabled + * @param lastUpdateTime Timestamp when the job was last updated + * @param lockDurationSeconds Time in seconds for how long this job should acquire a lock + */ public GreetJob( String name, Schedule schedule, @@ -79,6 +92,11 @@ public GreetJob( this.lockDurationSeconds = lockDurationSeconds; } + /** + * + * @param input The input stream + * @throws IOException Thrown if there is an error parsing the input stream into a GreetJob + */ public GreetJob(StreamInput input) throws IOException { name = input.readString(); if (input.readEnum(GreetJob.ScheduleType.class) == ScheduleType.CRON) { @@ -93,6 +111,13 @@ public GreetJob(StreamInput input) throws IOException { lockDurationSeconds = input.readLong(); } + /** + * + * @param builder An XContentBuilder instance + * @param params TOXContent.Params + * @return + * @throws IOException + */ @Override public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException { XContentBuilder xContentBuilder = builder.startObject() @@ -108,6 +133,11 @@ public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params par return xContentBuilder.endObject(); } + /** + * + * @param output The output stream + * @throws IOException + */ @Override public void writeTo(StreamOutput output) throws IOException { output.writeString(name); @@ -124,6 +154,12 @@ public void writeTo(StreamOutput output) throws IOException { output.writeLong(lockDurationSeconds); } + /** + * + * @param parser Parser that takes builds a GreetJob from XContent + * @return An instance of a GreetJob + * @throws IOException + */ public static GreetJob parse(XContentParser parser) throws IOException { String name = null; Schedule schedule = null; @@ -239,6 +275,10 @@ public static Instant toInstant(XContentParser parser) throws IOException { return null; } + /** + * + * @return Returns a plain old java object of a GreetJob for writing to the hello world jobs index + */ public GreetJobPojo toPojo() { GreetJobPojo.SchedulePojo.IntervalPojo interval = null; if (this.schedule instanceof IntervalSchedule) { @@ -258,6 +298,9 @@ public GreetJobPojo toPojo() { ); } + /** + * A plain java representation of a GreetJob using only primitives + */ public static class GreetJobPojo { public long enabled_time; public long last_update_time; @@ -270,6 +313,15 @@ public static class GreetJobPojo { public SchedulePojo schedule; + /** + * + * @param enabledTime + * @param lastUpdateTime + * @param name + * @param lockDurationSeconds + * @param enabled + * @param schedule + */ public GreetJobPojo( long enabledTime, long lastUpdateTime, @@ -286,14 +338,24 @@ public GreetJobPojo( this.schedule = schedule; } + /** + * A plain java representation of a Schedule using only primitives + */ public static class SchedulePojo { public IntervalPojo interval; + /** + * + * @param interval An Interval instance + */ public SchedulePojo(IntervalPojo interval) { this.interval = interval; } + /** + * A plain java representation of a Interval using only primitives + */ public static class IntervalPojo { public String unit; @@ -301,6 +363,12 @@ public static class IntervalPojo { public long start_time; + /** + * + * @param unit Unit of time + * @param period Number of units between job execution + * @param start_time The time when the interval first started + */ public IntervalPojo(String unit, int period, long start_time) { this.unit = unit; this.period = period; diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobParameterAction.java b/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobParameterAction.java index 0a8dd4ee..78d09714 100644 --- a/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobParameterAction.java +++ b/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobParameterAction.java @@ -12,6 +12,9 @@ import org.opensearch.action.ActionType; import org.opensearch.jobscheduler.transport.response.JobParameterResponse; +/** + * Hello World Job Parameter Action + */ public class HWJobParameterAction extends ActionType { public static final String NAME = "extensions:hw/greet_job_parameter"; public static final HWJobParameterAction INSTANCE = new HWJobParameterAction(); diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobParameterTransportAction.java b/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobParameterTransportAction.java index e2619bc1..e5b05243 100644 --- a/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobParameterTransportAction.java +++ b/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobParameterTransportAction.java @@ -31,12 +31,22 @@ import static org.opensearch.common.xcontent.XContentParserUtils.ensureExpectedToken; import static org.opensearch.sdk.sample.helloworld.util.RestHandlerUtils.wrapRestActionListener; +/** + * Hello World Job Parameter Transport Action + */ public class HWJobParameterTransportAction extends TransportAction { private static final Logger LOG = LogManager.getLogger(HWJobParameterTransportAction.class); private final SDKNamedXContentRegistry xContentRegistry; + /** + * Instantiate this action + * + * @param actionFilters Action filters + * @param taskManager The task manager + * @param xContentRegistry The xContentRegistry + */ @Inject protected HWJobParameterTransportAction( ActionFilters actionFilters, diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobRunnerAction.java b/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobRunnerAction.java index 4698dbe1..d9c41e97 100644 --- a/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobRunnerAction.java +++ b/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobRunnerAction.java @@ -12,6 +12,9 @@ import org.opensearch.action.ActionType; import org.opensearch.jobscheduler.transport.response.JobRunnerResponse; +/** + * Hello World Job Runner Action + */ public class HWJobRunnerAction extends ActionType { public static final String NAME = "extensions:hw/greet_job_runner"; diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobRunnerTransportAction.java b/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobRunnerTransportAction.java index c3695b12..bc3f290c 100644 --- a/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobRunnerTransportAction.java +++ b/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobRunnerTransportAction.java @@ -37,6 +37,9 @@ import static org.opensearch.common.xcontent.XContentParserUtils.ensureExpectedToken; import static org.opensearch.sdk.sample.helloworld.util.RestHandlerUtils.wrapRestActionListener; +/** + * Hello World Job Runner Transport Action + */ public class HWJobRunnerTransportAction extends TransportAction { private static final Logger LOG = LogManager.getLogger(HWJobRunnerTransportAction.class); @@ -44,6 +47,14 @@ public class HWJobRunnerTransportAction extends TransportAction ActionListener wrapRestActionListener(ActionListener action }); } + /** + * @param e Throwable + * @return Return a boolean whether there was an exception thrown on a request + */ public static boolean isBadRequest(Throwable e) { if (e == null) { return false; @@ -74,6 +78,10 @@ public static boolean isBadRequest(Throwable e) { return e instanceof IllegalArgumentException; } + /** + * @param e Throwable + * @return Return boolean if throwable is of a proper exception type + */ public static boolean isProperExceptionToReturn(Throwable e) { if (e == null) { return false; From da40af0446f8c14974ade3689968acc6289f7c08 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Mon, 1 May 2023 14:34:19 -0400 Subject: [PATCH 06/16] WIP on multi-project Signed-off-by: Craig Perkins --- build.gradle | 40 +- sample/.gitignore | 29 ++ sample/build.gradle | 86 ++++ .../ExampleCustomSettingConfig.java | 30 ++ .../helloworld/HelloWorldExtension.java | 118 ++++++ .../helloworld/rest/RestHelloAction.java | 133 ++++++ .../rest/RestRemoteHelloAction.java | 263 ++++++++++++ .../sample/helloworld/schedule/GreetJob.java | 380 ++++++++++++++++++ .../sdk/sample/helloworld/spec/openapi.json | 149 +++++++ .../sdk/sample/helloworld/spec/openapi.yaml | 117 ++++++ .../transport/HWJobParameterAction.java | 25 ++ .../HWJobParameterTransportAction.java | 81 ++++ .../transport/HWJobRunnerAction.java | 26 ++ .../transport/HWJobRunnerTransportAction.java | 164 ++++++++ .../transport/HelloWorldJobRunner.java | 47 +++ .../helloworld/transport/SampleAction.java | 32 ++ .../helloworld/transport/SampleRequest.java | 58 +++ .../helloworld/transport/SampleResponse.java | 52 +++ .../transport/SampleTransportAction.java | 46 +++ .../helloworld/util/RestHandlerUtils.java | 91 +++++ .../resources/mappings/hello-world-jobs.json | 50 +++ .../resources/sample/helloworld-settings.yml | 5 + settings.gradle | 4 + 23 files changed, 2002 insertions(+), 24 deletions(-) create mode 100644 sample/.gitignore create mode 100644 sample/build.gradle create mode 100644 sample/src/main/java/org/opensearch/sdk/sample/helloworld/ExampleCustomSettingConfig.java create mode 100644 sample/src/main/java/org/opensearch/sdk/sample/helloworld/HelloWorldExtension.java create mode 100644 sample/src/main/java/org/opensearch/sdk/sample/helloworld/rest/RestHelloAction.java create mode 100644 sample/src/main/java/org/opensearch/sdk/sample/helloworld/rest/RestRemoteHelloAction.java create mode 100644 sample/src/main/java/org/opensearch/sdk/sample/helloworld/schedule/GreetJob.java create mode 100644 sample/src/main/java/org/opensearch/sdk/sample/helloworld/spec/openapi.json create mode 100644 sample/src/main/java/org/opensearch/sdk/sample/helloworld/spec/openapi.yaml create mode 100644 sample/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobParameterAction.java create mode 100644 sample/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobParameterTransportAction.java create mode 100644 sample/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobRunnerAction.java create mode 100644 sample/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobRunnerTransportAction.java create mode 100644 sample/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HelloWorldJobRunner.java create mode 100644 sample/src/main/java/org/opensearch/sdk/sample/helloworld/transport/SampleAction.java create mode 100644 sample/src/main/java/org/opensearch/sdk/sample/helloworld/transport/SampleRequest.java create mode 100644 sample/src/main/java/org/opensearch/sdk/sample/helloworld/transport/SampleResponse.java create mode 100644 sample/src/main/java/org/opensearch/sdk/sample/helloworld/transport/SampleTransportAction.java create mode 100644 sample/src/main/java/org/opensearch/sdk/sample/helloworld/util/RestHandlerUtils.java create mode 100644 sample/src/main/resources/mappings/hello-world-jobs.json create mode 100644 sample/src/main/resources/sample/helloworld-settings.yml diff --git a/build.gradle b/build.gradle index faf99f87..b334d6e4 100644 --- a/build.gradle +++ b/build.gradle @@ -14,6 +14,7 @@ import org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestFram plugins { id 'java' + id 'java-library' id "com.diffplug.spotless" version "6.18.0" apply false id 'jacoco' id "com.form.diff-coverage" version "0.9.5" @@ -33,12 +34,6 @@ apply plugin: 'application' apply from: 'gradle/formatting.gradle' apply plugin: 'maven-publish' -// Temporary to keep "gradle run" working -// TODO: change this to an extension designed for testing instead of duplicating a sample -// https://github.com/opensearch-project/opensearch-sdk-java/issues/175 -mainClassName = 'org.opensearch.sdk.sample.helloworld.HelloWorldExtension' - - group 'org.opensearch.sdk' version '2.0.0-SNAPSHOT' @@ -77,6 +72,15 @@ repositories { maven { url "https://d1nvenhzbhpy0q.cloudfront.net/snapshots/lucene/"} } +subprojects { + repositories { + mavenLocal() + mavenCentral() + maven { url "https://aws.oss.sonatype.org/content/repositories/snapshots" } + maven { url "https://d1nvenhzbhpy0q.cloudfront.net/snapshots/lucene/"} + } +} + dependencies { def opensearchVersion = "3.0.0-SNAPSHOT" @@ -91,18 +95,14 @@ dependencies { def junitPlatform = "1.9.3" def jaxbVersion = "2.3.1" - implementation("org.opensearch:opensearch:${opensearchVersion}") - // job-scheduler dependency is added for hello world sample extension - implementation("org.opensearch.plugin:opensearch-job-scheduler:${jobSchedulerVersion}") - implementation("org.opensearch:opensearch-job-scheduler:${jobSchedulerVersion}") - implementation "org.opensearch:opensearch-job-scheduler-spi:${jobSchedulerVersion}" + api("org.opensearch:opensearch:${opensearchVersion}") implementation("org.apache.logging.log4j:log4j-api:${log4jVersion}") implementation("org.apache.logging.log4j:log4j-core:${log4jVersion}") implementation("org.apache.logging.log4j:log4j-jul:${log4jVersion}") - implementation("org.opensearch.client:opensearch-rest-high-level-client:${opensearchVersion}") - implementation("org.opensearch.client:opensearch-rest-client:${opensearchVersion}") - implementation("org.opensearch.client:opensearch-java:${opensearchVersion}") + api("org.opensearch.client:opensearch-rest-high-level-client:${opensearchVersion}") + api("org.opensearch.client:opensearch-rest-client:${opensearchVersion}") + api("org.opensearch.client:opensearch-java:${opensearchVersion}") implementation("org.opensearch.plugin:transport-netty4-client:${opensearchVersion}") implementation("io.netty:netty-all:${nettyVersion}") testCompileOnly("junit:junit:${junit4Version}") { @@ -115,25 +115,17 @@ dependencies { implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${jacksonDatabindVersion}") implementation("com.fasterxml.jackson.datatype:jackson-datatype-guava:${jacksonDatabindVersion}") constraints { - implementation("com.google.guava:guava:${guavaVersion}") { + api("com.google.guava:guava:${guavaVersion}") { because 'versions below 30.0 have active CVE' } } - implementation("com.google.inject:guice:${guiceVersion}") + api("com.google.inject:guice:${guiceVersion}") testImplementation("org.junit.jupiter:junit-jupiter-api:${junit5Version}") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${junit5Version}") testImplementation("org.opensearch.test:framework:${opensearchVersion}") testRuntimeOnly("org.junit.platform:junit-platform-launcher:${junitPlatform}") } -// this task runs the helloworld sample extension -task helloWorld(type: JavaExec) { - group = 'Execution' - description = 'Run HelloWorld Extension.' - mainClass = 'org.opensearch.sdk.sample.helloworld.HelloWorldExtension' - classpath = sourceSets.main.runtimeClasspath -} - test { // Temporary workaround for https://github.com/gradle/gradle/issues/23995 getTestFrameworkProperty().convention(getProviderFactory().provider(() -> new JUnitPlatformTestFramework(it.getFilter(), false))) diff --git a/sample/.gitignore b/sample/.gitignore new file mode 100644 index 00000000..f68d1099 --- /dev/null +++ b/sample/.gitignore @@ -0,0 +1,29 @@ +### IntelliJ IDEA ### +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/sample/build.gradle b/sample/build.gradle new file mode 100644 index 00000000..7b547ef8 --- /dev/null +++ b/sample/build.gradle @@ -0,0 +1,86 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import java.nio.file.Files + +plugins { + id 'java' + id "com.diffplug.spotless" version "6.18.0" apply false + id 'jacoco' + id "com.form.diff-coverage" version "0.9.5" + // for javadocs and checks spotless doesn't do + id 'checkstyle' +} + + +ext { + projectSubstitutions = [:] + licenseFile = rootProject.file('LICENSE.txt') + noticeFile = rootProject.file('NOTICE.txt') +} + + +apply plugin: 'application' +apply plugin: 'maven-publish' + +// Temporary to keep "gradle run" working +// TODO: change this to an extension designed for testing instead of duplicating a sample +// https://github.com/opensearch-project/opensearch-sdk-java/issues/175 +mainClassName = 'org.opensearch.sdk.sample.helloworld.HelloWorldExtension' + + +group 'org.opensearch.sdk.sample' +version '2.0.0-SNAPSHOT' + +java { + withSourcesJar() + withJavadocJar() +} + +publishing { + publications { + group = "${group}" + version = "${version}" + mavenJava(MavenPublication) { + from components.java + } + sourceCompatibility = 11 + targetCompatibility = 11 + } + + repositories { + maven { + name = "Snapshots" // optional target repository name + url = "https://aws.oss.sonatype.org/content/repositories/snapshots" + credentials { + username "$System.env.SONATYPE_USERNAME" + password "$System.env.SONATYPE_PASSWORD" + } + } + } +} + +dependencies { + def opensearchVersion = "3.0.0-SNAPSHOT" + def jobSchedulerVersion = "3.0.0.0-SNAPSHOT" + implementation project(':') + implementation("org.opensearch.plugin:opensearch-job-scheduler:${jobSchedulerVersion}") + implementation("org.opensearch:opensearch-job-scheduler:${jobSchedulerVersion}") + implementation "org.opensearch:opensearch-job-scheduler-spi:${jobSchedulerVersion}" +} + +// this task runs the helloworld sample extension +task helloWorld(type: JavaExec) { + group = 'Execution' + description = 'Run HelloWorld Extension.' + mainClass = 'org.opensearch.sdk.sample.helloworld.HelloWorldExtension' + classpath = sourceSets.main.runtimeClasspath +} diff --git a/sample/src/main/java/org/opensearch/sdk/sample/helloworld/ExampleCustomSettingConfig.java b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/ExampleCustomSettingConfig.java new file mode 100644 index 00000000..b5c7e38e --- /dev/null +++ b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/ExampleCustomSettingConfig.java @@ -0,0 +1,30 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.sdk.sample.helloworld; + +import org.opensearch.common.settings.Setting; +import org.opensearch.common.settings.Setting.RegexValidator; +import org.opensearch.common.settings.Setting.Property; + +/** + * {@link ExampleCustomSettingConfig} contains the custom settings value and their static declarations. + */ +public class ExampleCustomSettingConfig { + private static final String FORBIDDEN_VALUE = "forbidden"; + /** + * A string setting. If the string setting matches the FORBIDDEN_REGEX string and parameter isMatching is true the validation will fail. + */ + public static final Setting VALIDATED_SETTING = Setting.simpleString( + "custom.validate", + new RegexValidator(FORBIDDEN_VALUE, false), + Property.NodeScope, + Property.Dynamic + ); +} diff --git a/sample/src/main/java/org/opensearch/sdk/sample/helloworld/HelloWorldExtension.java b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/HelloWorldExtension.java new file mode 100644 index 00000000..598b12a0 --- /dev/null +++ b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/HelloWorldExtension.java @@ -0,0 +1,118 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.sdk.sample.helloworld; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import com.google.common.collect.ImmutableList; +import org.opensearch.action.ActionRequest; +import org.opensearch.action.ActionResponse; +import org.opensearch.common.settings.Setting; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.sdk.BaseExtension; +import org.opensearch.sdk.Extension; +import org.opensearch.sdk.ExtensionSettings; +import org.opensearch.sdk.ExtensionsRunner; +import org.opensearch.sdk.SDKClient; +import org.opensearch.sdk.api.ActionExtension; +import org.opensearch.sdk.rest.ExtensionRestHandler; +import org.opensearch.sdk.sample.helloworld.ExampleCustomSettingConfig; +import org.opensearch.sdk.sample.helloworld.rest.RestHelloAction; +import org.opensearch.sdk.sample.helloworld.rest.RestRemoteHelloAction; +import org.opensearch.sdk.sample.helloworld.schedule.GreetJob; +import org.opensearch.sdk.sample.helloworld.transport.HWJobParameterAction; +import org.opensearch.sdk.sample.helloworld.transport.HWJobParameterTransportAction; +import org.opensearch.sdk.sample.helloworld.transport.HWJobRunnerAction; +import org.opensearch.sdk.sample.helloworld.transport.HWJobRunnerTransportAction; +import org.opensearch.sdk.sample.helloworld.transport.SampleAction; +import org.opensearch.sdk.sample.helloworld.transport.SampleTransportAction; + +/** + * Sample class to demonstrate how to use the OpenSearch SDK for Java to create + * an extension. + *

+ * To create your own extension, implement the {@link #getExtensionSettings()} and {@link #getExtensionRestHandlers()} methods. + * You may either create an {@link ExtensionSettings} object directly with the constructor, or read it from a YAML file on your class path. + *

+ * To execute, pass an instatiated object of this class to {@link ExtensionsRunner#run(Extension)}. + */ +public class HelloWorldExtension extends BaseExtension implements ActionExtension { + + /** + * Optional classpath-relative path to a yml file containing extension settings. + */ + private static final String EXTENSION_SETTINGS_PATH = "/sample/helloworld-settings.yml"; + + /** + * Instantiate this extension, initializing the connection settings and REST actions. + * The Extension must provide its settings to the ExtensionsRunner. + * These may be optionally read from a YAML file on the class path. + * Or you may directly instantiate with the ExtensionSettings constructor. + * + */ + public HelloWorldExtension() { + super(EXTENSION_SETTINGS_PATH); + } + + @Override + public List getExtensionRestHandlers() { + return List.of(new RestHelloAction(), new RestRemoteHelloAction(extensionsRunner())); + } + + @Override + public List> getActions() { + return Arrays.asList( + new ActionHandler<>(SampleAction.INSTANCE, SampleTransportAction.class), + new ActionHandler<>(HWJobRunnerAction.INSTANCE, HWJobRunnerTransportAction.class), + new ActionHandler<>(HWJobParameterAction.INSTANCE, HWJobParameterTransportAction.class) + ); + } + + @Override + public List getNamedXContent() { + return ImmutableList.of(GreetJob.XCONTENT_REGISTRY); + } + + @Deprecated + private SDKClient.SDKRestClient createRestClient(ExtensionsRunner runner) { + @SuppressWarnings("resource") + SDKClient.SDKRestClient client = runner.getSdkClient().initializeRestClient(); + return client; + } + + @Override + public Collection createComponents(ExtensionsRunner runner) { + SDKClient.SDKRestClient sdkRestClient = createRestClient(runner); + + return Collections.singletonList(sdkRestClient); + } + + /** + * A list of object that includes a single instance of Validator for Custom Setting + */ + public List> getSettings() { + return Arrays.asList(ExampleCustomSettingConfig.VALIDATED_SETTING); + } + + /** + * Entry point to execute an extension. + * + * @param args Unused. + * @throws IOException on a failure in the ExtensionsRunner + */ + public static void main(String[] args) throws IOException { + // Execute this extension by instantiating it and passing to ExtensionsRunner + ExtensionsRunner.run(new HelloWorldExtension()); + } +} diff --git a/sample/src/main/java/org/opensearch/sdk/sample/helloworld/rest/RestHelloAction.java b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/rest/RestHelloAction.java new file mode 100644 index 00000000..8673d48c --- /dev/null +++ b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/rest/RestHelloAction.java @@ -0,0 +1,133 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.sdk.sample.helloworld.rest; + +import org.opensearch.OpenSearchParseException; +import org.opensearch.common.bytes.BytesReference; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.common.xcontent.json.JsonXContent; +import org.opensearch.extensions.rest.ExtensionRestResponse; +import org.opensearch.rest.RestRequest; +import org.opensearch.sdk.rest.BaseExtensionRestHandler; +import org.opensearch.sdk.rest.ExtensionRestHandler; +import java.io.IOException; +import java.net.URLDecoder; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.function.Function; + +import static org.opensearch.rest.RestRequest.Method.DELETE; +import static org.opensearch.rest.RestRequest.Method.GET; +import static org.opensearch.rest.RestRequest.Method.POST; +import static org.opensearch.rest.RestRequest.Method.PUT; +import static org.opensearch.rest.RestStatus.BAD_REQUEST; +import static org.opensearch.rest.RestStatus.NOT_ACCEPTABLE; +import static org.opensearch.rest.RestStatus.OK; + +/** + * Sample REST Handler (REST Action). + * Extension REST handlers must implement {@link ExtensionRestHandler}. + * Extending {@link BaseExtensionRestHandler} provides many convenience methods. + */ +public class RestHelloAction extends BaseExtensionRestHandler { + + private static final String TEXT_CONTENT_TYPE = "text/plain; charset=UTF-8"; + private static final String GREETING = "Hello, %s!"; + private static final String DEFAULT_NAME = "World"; + + private String worldName = DEFAULT_NAME; + private List worldAdjectives = new ArrayList<>(); + private Random rand = new Random(); + + @Override + public List routeHandlers() { + return List.of( + new RouteHandler(GET, "/hello", handleGetRequest), + new RouteHandler(POST, "/hello", handlePostRequest), + new RouteHandler(PUT, "/hello/{name}", handlePutRequest), + new RouteHandler(DELETE, "/goodbye", handleDeleteRequest) + ); + } + + private Function handleGetRequest = (request) -> { + String worldNameWithRandomAdjective = worldAdjectives.isEmpty() + ? worldName + : String.join(" ", worldAdjectives.get(rand.nextInt(worldAdjectives.size())), worldName); + return new ExtensionRestResponse(request, OK, String.format(GREETING, worldNameWithRandomAdjective)); + }; + + private Function handlePostRequest = (request) -> { + if (request.hasContent()) { + String adjective = ""; + XContentType contentType = request.getXContentType(); + if (contentType == null) { + // Plain text + adjective = request.content().utf8ToString(); + } else if (contentType.equals(XContentType.JSON)) { + try { + adjective = request.contentParser().mapStrings().get("adjective"); + } catch (IOException | OpenSearchParseException e) { + // Sample plain text response + return new ExtensionRestResponse(request, BAD_REQUEST, "Unable to parse adjective from JSON"); + } + } else { + // Sample text response with content type + return new ExtensionRestResponse( + request, + NOT_ACCEPTABLE, + TEXT_CONTENT_TYPE, + "Only text and JSON content types are supported" + ); + } + if (adjective != null && !adjective.isBlank()) { + worldAdjectives.add(adjective.trim()); + // Sample JSON response with a builder + try { + XContentBuilder builder = JsonXContent.contentBuilder() + .startObject() + .field("worldAdjectives", worldAdjectives) + .endObject(); + return new ExtensionRestResponse(request, OK, builder); + } catch (IOException e) { + // Sample response for developer error + return unhandledRequest(request); + } + } + byte[] noAdjective = "No adjective included with POST request".getBytes(StandardCharsets.UTF_8); + // Sample binary response with content type + return new ExtensionRestResponse(request, BAD_REQUEST, TEXT_CONTENT_TYPE, noAdjective); + } + // Sample bytes reference response with content type + BytesReference noContent = BytesReference.fromByteBuffer( + ByteBuffer.wrap("No content included with POST request".getBytes(StandardCharsets.UTF_8)) + ); + return new ExtensionRestResponse(request, BAD_REQUEST, TEXT_CONTENT_TYPE, noContent); + }; + + private Function handlePutRequest = (request) -> { + String name = request.param("name"); + try { + worldName = URLDecoder.decode(name, StandardCharsets.UTF_8); + } catch (IllegalArgumentException e) { + return new ExtensionRestResponse(request, BAD_REQUEST, e.getMessage()); + } + return new ExtensionRestResponse(request, OK, "Updated the world's name to " + worldName); + }; + + private Function handleDeleteRequest = (request) -> { + this.worldName = DEFAULT_NAME; + this.worldAdjectives.clear(); + return new ExtensionRestResponse(request, OK, "Goodbye, cruel world! Restored default values."); + }; +} diff --git a/sample/src/main/java/org/opensearch/sdk/sample/helloworld/rest/RestRemoteHelloAction.java b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/rest/RestRemoteHelloAction.java new file mode 100644 index 00000000..be6e1220 --- /dev/null +++ b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/rest/RestRemoteHelloAction.java @@ -0,0 +1,263 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.sdk.sample.helloworld.rest; + +import com.google.common.io.Resources; +import jakarta.json.stream.JsonParser; +import org.apache.commons.codec.Charsets; +import org.opensearch.OpenSearchException; +import org.opensearch.ResourceAlreadyExistsException; +import org.opensearch.action.ActionListener; +import org.opensearch.client.Request; +import org.opensearch.client.Response; +import org.opensearch.client.WarningFailureException; +import org.opensearch.client.json.JsonpMapper; +import org.opensearch.client.json.jackson.JacksonJsonpMapper; +import org.opensearch.client.json.jsonb.JsonbJsonpMapper; +import org.opensearch.client.opensearch.OpenSearchClient; +import org.opensearch.client.opensearch._types.mapping.TypeMapping; +import org.opensearch.client.opensearch.core.IndexRequest; +import org.opensearch.client.opensearch.indices.CreateIndexRequest; +import org.opensearch.client.opensearch.indices.GetMappingResponse; +import org.opensearch.client.opensearch.indices.OpenSearchIndicesClient; +import org.opensearch.common.Strings; +import org.opensearch.common.io.stream.StreamInput; +import org.opensearch.common.xcontent.json.JsonXContent; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.extensions.ExtensionsManager; +import org.opensearch.extensions.action.RemoteExtensionActionResponse; +import org.opensearch.extensions.rest.ExtensionRestResponse; +import org.opensearch.jobscheduler.JobSchedulerPlugin; +import org.opensearch.jobscheduler.rest.request.GetJobDetailsRequest; +import org.opensearch.jobscheduler.spi.schedule.IntervalSchedule; +import org.opensearch.jobscheduler.spi.schedule.Schedule; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.RestStatus; +import org.opensearch.sdk.ExtensionsRunner; +import org.opensearch.sdk.SDKClient; +import org.opensearch.sdk.action.RemoteExtensionAction; +import org.opensearch.sdk.action.RemoteExtensionActionRequest; +import org.opensearch.sdk.rest.BaseExtensionRestHandler; +import org.opensearch.sdk.sample.helloworld.schedule.GreetJob; +import org.opensearch.sdk.sample.helloworld.transport.HWJobParameterAction; +import org.opensearch.sdk.sample.helloworld.transport.HWJobRunnerAction; +import org.opensearch.sdk.sample.helloworld.transport.SampleAction; +import org.opensearch.sdk.sample.helloworld.transport.SampleRequest; +import org.opensearch.sdk.sample.helloworld.transport.SampleResponse; + +import java.io.IOException; +import java.io.StringReader; +import java.net.URL; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Locale; +import java.util.Random; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +import static org.opensearch.rest.RestRequest.Method.GET; +import static org.opensearch.rest.RestRequest.Method.PUT; +import static org.opensearch.rest.RestStatus.OK; + +/** + * Sample REST Handler demonstrating proxy actions to another extension + */ +public class RestRemoteHelloAction extends BaseExtensionRestHandler { + private ExtensionsRunner extensionsRunner; + + /** + * Instantiate this action + * + * @param runner The ExtensionsRunner instance + */ + public RestRemoteHelloAction(ExtensionsRunner runner) { + this.extensionsRunner = runner; + } + + @Override + public List routeHandlers() { + return List.of( + new RouteHandler(GET, "/hello/{name}", handleRemoteGetRequest), + new RouteHandler(PUT, "/schedule/hello", handleScheduleRequest) + ); + } + + private void registerJobDetails(SDKClient.SDKRestClient client) throws IOException { + + XContentBuilder requestBody = JsonXContent.contentBuilder(); + requestBody.startObject(); + requestBody.field(GetJobDetailsRequest.JOB_INDEX, GreetJob.HELLO_WORLD_JOB_INDEX); + requestBody.field(GetJobDetailsRequest.JOB_TYPE, GreetJob.PARSE_FIELD_NAME); + requestBody.field(GetJobDetailsRequest.JOB_PARAMETER_ACTION, HWJobParameterAction.class.getName()); + requestBody.field(GetJobDetailsRequest.JOB_RUNNER_ACTION, HWJobRunnerAction.class.getName()); + requestBody.field(GetJobDetailsRequest.EXTENSION_UNIQUE_ID, extensionsRunner.getUniqueId()); + requestBody.endObject(); + + Request request = new Request("PUT", String.format(Locale.ROOT, "%s/%s", JobSchedulerPlugin.JS_BASE_URI, "_job_details")); + request.setJsonEntity(Strings.toString(requestBody)); + + Response response = client.performRequest(request); + boolean registeredJobDetails = RestStatus.fromCode(response.getStatusLine().getStatusCode()) == RestStatus.OK ? true : false; + } + + /** + * Get hello world job index mapping json content. + * + * @return hello world job index mapping + * @throws IOException IOException if mapping file can't be read correctly + */ + public static String getHelloWorldJobMappings() throws IOException { + URL url = RestRemoteHelloAction.class.getClassLoader().getResource("mappings/hello-world-jobs.json"); + return Resources.toString(url, Charsets.UTF_8); + } + + private JsonpMapper setupMapper(int rand) { + // Randomly choose json-b or jackson + if (rand % 2 == 0) { + return new JsonbJsonpMapper() { + @Override + public boolean ignoreUnknownFields() { + return false; + } + }; + } else { + return new JacksonJsonpMapper() { + @Override + public boolean ignoreUnknownFields() { + return false; + } + }; + } + } + + /** + * Deserializes json string into a java object + * + * @param json The JSON string + * @param clazz The class to deserialize to + * @param The class to deserialize to + * + * @return Object instance of clazz requested + */ + public T fromJson(String json, Class clazz) { + int rand = new Random().nextInt(); + JsonpMapper mapper = setupMapper(rand); + JsonParser parser = mapper.jsonProvider().createParser(new StringReader(json)); + return mapper.deserialize(parser, clazz); + } + + private Function handleScheduleRequest = (request) -> { + SDKClient client = extensionsRunner.getSdkClient(); + SDKClient.SDKRestClient restClient = client.initializeRestClient(); + OpenSearchClient javaClient = client.initializeJavaClient(); + + try { + registerJobDetails(restClient); + } catch (WarningFailureException e) { + // ignore + } catch (Exception e) { + if (e instanceof ResourceAlreadyExistsException + || e.getCause() instanceof ResourceAlreadyExistsException + || e.getMessage().contains("resource_already_exists_exception")) { + // ignore + } else { + // Catch all other OpenSearchExceptions + return new ExtensionRestResponse(request, RestStatus.INTERNAL_SERVER_ERROR, e.getMessage()); + } + } + + try { + String mappingsJson = getHelloWorldJobMappings(); + GetMappingResponse response = fromJson(mappingsJson, GetMappingResponse.class); + TypeMapping mappings = response.get(GreetJob.HELLO_WORLD_JOB_INDEX).mappings(); + + CreateIndexRequest cir = new CreateIndexRequest.Builder().index(GreetJob.HELLO_WORLD_JOB_INDEX).mappings(mappings).build(); + + OpenSearchIndicesClient indicesClient = javaClient.indices(); + indicesClient.create(cir); + } catch (IOException e) { + return new ExtensionRestResponse(request, RestStatus.INTERNAL_SERVER_ERROR, e.getMessage()); + } catch (WarningFailureException e) { + // TODO This is failing on ConvertResponse. Ignoring. + /* + * org.opensearch.transport.RemoteTransportException: [hello-world][127.0.0.1:4532][internal:extensions/restexecuteonextensiontaction] + * Caused by: org.opensearch.common.io.stream.NotSerializableExceptionWrapper: warning_failure_exception: method [PUT], host [https://127.0.0.1:9200], URI [/.hello-world-jobs], status line [HTTP/2.0 200 OK] + * Warnings: [index name [.hello-world-jobs] starts with a dot '.', in the next major version, index names starting with a dot are reserved for hidden indices and system indices, this request accesses system indices: [.opendistro_security], but in a future major version, direct access to system indices will be prevented by default] + * {"acknowledged":true,"shards_acknowledged":true,"index":".hello-world-jobs"} + */ + } catch (OpenSearchException e) { + if (e instanceof ResourceAlreadyExistsException || e.getCause() instanceof ResourceAlreadyExistsException) {} else { + // Catch all other OpenSearchExceptions + return new ExtensionRestResponse(request, RestStatus.INTERNAL_SERVER_ERROR, e.getMessage()); + } + } catch (Exception e) { + if (e.getMessage().contains("resource_already_exists_exception")) {} else { + // Catch all other OpenSearchExceptions + return new ExtensionRestResponse(request, RestStatus.INTERNAL_SERVER_ERROR, e.getMessage()); + } + } + + Schedule schedule = new IntervalSchedule(Instant.now(), 1, ChronoUnit.MINUTES); + Duration duration = Duration.of(1, ChronoUnit.MINUTES); + + GreetJob job = new GreetJob("hw", schedule, true, Instant.now(), null, Instant.now(), duration.getSeconds()); + + try { + // Reference: AnomalyDetector - IndexAnomalyDetectorJobActionHandler.indexAnomalyDetectorJob + IndexRequest ir = new IndexRequest.Builder().index(GreetJob.HELLO_WORLD_JOB_INDEX).document(job.toPojo()).build(); + javaClient.index(ir); + } catch (IOException e) { + return new ExtensionRestResponse(request, RestStatus.INTERNAL_SERVER_ERROR, e.getMessage()); + } + + return new ExtensionRestResponse(request, OK, "GreetJob successfully scheduled"); + }; + + private Function handleRemoteGetRequest = (request) -> { + SDKClient client = extensionsRunner.getSdkClient(); + + String name = request.param("name"); + // Create a request using class on remote + // This class happens to be local for simplicity but is a class on the remote extension + SampleRequest sampleRequest = new SampleRequest(name); + + // Serialize this request in a proxy action request + // This requires that the remote extension has a corresponding transport action registered + // This Action class happens to be local for simplicity but is a class on the remote extension + RemoteExtensionActionRequest proxyActionRequest = new RemoteExtensionActionRequest(SampleAction.INSTANCE, sampleRequest); + + // TODO: We need async client.execute to hide these action listener details and return the future directly + // https://github.com/opensearch-project/opensearch-sdk-java/issues/584 + CompletableFuture futureResponse = new CompletableFuture<>(); + client.execute( + RemoteExtensionAction.INSTANCE, + proxyActionRequest, + ActionListener.wrap(r -> futureResponse.complete(r), e -> futureResponse.completeExceptionally(e)) + ); + try { + RemoteExtensionActionResponse response = futureResponse.orTimeout( + ExtensionsManager.EXTENSION_REQUEST_WAIT_TIMEOUT, + TimeUnit.SECONDS + ).get(); + if (!response.isSuccess()) { + return new ExtensionRestResponse(request, OK, "Remote extension reponse failed: " + response.getResponseBytesAsString()); + } + // Parse out the expected response class from the bytes + SampleResponse sampleResponse = new SampleResponse(StreamInput.wrap(response.getResponseBytes())); + return new ExtensionRestResponse(request, OK, "Received greeting from remote extension: " + sampleResponse.getGreeting()); + } catch (Exception e) { + return exceptionalRequest(request, e); + } + }; + +} diff --git a/sample/src/main/java/org/opensearch/sdk/sample/helloworld/schedule/GreetJob.java b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/schedule/GreetJob.java new file mode 100644 index 00000000..01c66837 --- /dev/null +++ b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/schedule/GreetJob.java @@ -0,0 +1,380 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.sdk.sample.helloworld.schedule; + +import com.google.common.base.Objects; +import org.opensearch.common.io.stream.StreamInput; +import org.opensearch.common.io.stream.StreamOutput; +import org.opensearch.common.io.stream.Writeable; +import org.opensearch.core.ParseField; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.core.xcontent.ToXContentObject; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.jobscheduler.spi.ScheduledJobParameter; +import org.opensearch.jobscheduler.spi.schedule.CronSchedule; +import org.opensearch.jobscheduler.spi.schedule.IntervalSchedule; +import org.opensearch.jobscheduler.spi.schedule.Schedule; +import org.opensearch.jobscheduler.spi.schedule.ScheduleParser; + +import java.io.IOException; +import java.time.Instant; + +import static org.opensearch.common.xcontent.XContentParserUtils.ensureExpectedToken; + +/** + * Sample scheduled job for the HelloWorld Extension + */ +public class GreetJob implements Writeable, ToXContentObject, ScheduledJobParameter { + enum ScheduleType { + CRON, + INTERVAL + } + + public static final String PARSE_FIELD_NAME = "GreetJob"; + public static final NamedXContentRegistry.Entry XCONTENT_REGISTRY = new NamedXContentRegistry.Entry( + GreetJob.class, + new ParseField(PARSE_FIELD_NAME), + it -> parse(it) + ); + + public static final String HELLO_WORLD_JOB_INDEX = ".hello-world-jobs"; + public static final String NAME_FIELD = "name"; + public static final String LAST_UPDATE_TIME_FIELD = "last_update_time"; + public static final String LOCK_DURATION_SECONDS = "lock_duration_seconds"; + + public static final String SCHEDULE_FIELD = "schedule"; + public static final String IS_ENABLED_FIELD = "enabled"; + public static final String ENABLED_TIME_FIELD = "enabled_time"; + public static final String DISABLED_TIME_FIELD = "disabled_time"; + + private final String name; + private final Schedule schedule; + private final Boolean isEnabled; + private final Instant enabledTime; + private final Instant disabledTime; + private final Instant lastUpdateTime; + private final Long lockDurationSeconds; + + /** + * + * @param name name of the scheduled job + * @param schedule The schedule, cron or interval, the job run will run with + * @param isEnabled Flag to indices whether this job is enabled + * @param enabledTime Timestamp when the job was last enabled + * @param disabledTime Timestamp when the job was last disabled + * @param lastUpdateTime Timestamp when the job was last updated + * @param lockDurationSeconds Time in seconds for how long this job should acquire a lock + */ + public GreetJob( + String name, + Schedule schedule, + Boolean isEnabled, + Instant enabledTime, + Instant disabledTime, + Instant lastUpdateTime, + Long lockDurationSeconds + ) { + this.name = name; + this.schedule = schedule; + this.isEnabled = isEnabled; + this.enabledTime = enabledTime; + this.disabledTime = disabledTime; + this.lastUpdateTime = lastUpdateTime; + this.lockDurationSeconds = lockDurationSeconds; + } + + /** + * + * @param input The input stream + * @throws IOException Thrown if there is an error parsing the input stream into a GreetJob + */ + public GreetJob(StreamInput input) throws IOException { + name = input.readString(); + if (input.readEnum(ScheduleType.class) == ScheduleType.CRON) { + schedule = new CronSchedule(input); + } else { + schedule = new IntervalSchedule(input); + } + isEnabled = input.readBoolean(); + enabledTime = input.readInstant(); + disabledTime = input.readInstant(); + lastUpdateTime = input.readInstant(); + lockDurationSeconds = input.readLong(); + } + + /** + * + * @param builder An XContentBuilder instance + * @param params TOXContent.Params + * @return + * @throws IOException + */ + @Override + public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException { + XContentBuilder xContentBuilder = builder.startObject() + .field(NAME_FIELD, name) + .field(SCHEDULE_FIELD, schedule) + .field(IS_ENABLED_FIELD, isEnabled) + .field(ENABLED_TIME_FIELD, enabledTime.toEpochMilli()) + .field(LAST_UPDATE_TIME_FIELD, lastUpdateTime.toEpochMilli()) + .field(LOCK_DURATION_SECONDS, lockDurationSeconds); + if (disabledTime != null) { + xContentBuilder.field(DISABLED_TIME_FIELD, disabledTime.toEpochMilli()); + } + return xContentBuilder.endObject(); + } + + /** + * + * @param output The output stream + * @throws IOException + */ + @Override + public void writeTo(StreamOutput output) throws IOException { + output.writeString(name); + if (schedule instanceof CronSchedule) { + output.writeEnum(ScheduleType.CRON); + } else { + output.writeEnum(ScheduleType.INTERVAL); + } + schedule.writeTo(output); + output.writeBoolean(isEnabled); + output.writeInstant(enabledTime); + output.writeInstant(disabledTime); + output.writeInstant(lastUpdateTime); + output.writeLong(lockDurationSeconds); + } + + /** + * + * @param parser Parser that takes builds a GreetJob from XContent + * @return An instance of a GreetJob + * @throws IOException + */ + public static GreetJob parse(XContentParser parser) throws IOException { + String name = null; + Schedule schedule = null; + // we cannot set it to null as isEnabled() would do the unboxing and results in null pointer exception + Boolean isEnabled = Boolean.FALSE; + Instant enabledTime = null; + Instant disabledTime = null; + Instant lastUpdateTime = null; + Long lockDurationSeconds = 5L; + + ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.currentToken(), parser); + while (parser.nextToken() != XContentParser.Token.END_OBJECT) { + String fieldName = parser.currentName(); + parser.nextToken(); + + switch (fieldName) { + case NAME_FIELD: + name = parser.text(); + break; + case SCHEDULE_FIELD: + schedule = ScheduleParser.parse(parser); + break; + case IS_ENABLED_FIELD: + isEnabled = parser.booleanValue(); + break; + case ENABLED_TIME_FIELD: + enabledTime = toInstant(parser); + break; + case DISABLED_TIME_FIELD: + disabledTime = toInstant(parser); + break; + case LAST_UPDATE_TIME_FIELD: + lastUpdateTime = toInstant(parser); + break; + case LOCK_DURATION_SECONDS: + lockDurationSeconds = parser.longValue(); + break; + default: + parser.skipChildren(); + break; + } + } + return new GreetJob(name, schedule, isEnabled, enabledTime, disabledTime, lastUpdateTime, lockDurationSeconds); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + GreetJob that = (GreetJob) o; + return Objects.equal(getName(), that.getName()) + && Objects.equal(getSchedule(), that.getSchedule()) + && Objects.equal(isEnabled(), that.isEnabled()) + && Objects.equal(getEnabledTime(), that.getEnabledTime()) + && Objects.equal(getDisabledTime(), that.getDisabledTime()) + && Objects.equal(getLastUpdateTime(), that.getLastUpdateTime()) + && Objects.equal(getLockDurationSeconds(), that.getLockDurationSeconds()); + } + + @Override + public int hashCode() { + return Objects.hashCode(name, schedule, isEnabled, enabledTime, lastUpdateTime); + } + + @Override + public String getName() { + return name; + } + + @Override + public Schedule getSchedule() { + return schedule; + } + + @Override + public boolean isEnabled() { + return isEnabled; + } + + @Override + public Instant getEnabledTime() { + return enabledTime; + } + + public Instant getDisabledTime() { + return disabledTime; + } + + @Override + public Instant getLastUpdateTime() { + return lastUpdateTime; + } + + @Override + public Long getLockDurationSeconds() { + return lockDurationSeconds; + } + + /** + * Parse content parser to {@link Instant}. + * + * @param parser json based content parser + * @return instance of {@link Instant} + * @throws IOException IOException if content can't be parsed correctly + */ + public static Instant toInstant(XContentParser parser) throws IOException { + if (parser.currentToken() == null || parser.currentToken() == XContentParser.Token.VALUE_NULL) { + return null; + } + if (parser.currentToken().isValue()) { + return Instant.ofEpochMilli(parser.longValue()); + } + return null; + } + + /** + * + * @return Returns a plain old java object of a GreetJob for writing to the hello world jobs index + */ + public GreetJobPojo toPojo() { + GreetJobPojo.SchedulePojo.IntervalPojo interval = null; + if (this.schedule instanceof IntervalSchedule) { + interval = new GreetJobPojo.SchedulePojo.IntervalPojo( + ((IntervalSchedule) this.schedule).getUnit().toString(), + ((IntervalSchedule) this.schedule).getInterval(), + ((IntervalSchedule) this.schedule).getStartTime().toEpochMilli() + ); + } + return new GreetJobPojo( + this.enabledTime.toEpochMilli(), + this.lastUpdateTime.toEpochMilli(), + this.name, + this.lockDurationSeconds.intValue(), + this.isEnabled.booleanValue(), + new GreetJobPojo.SchedulePojo(interval) + ); + } + + /** + * A plain java representation of a GreetJob using only primitives + */ + public static class GreetJobPojo { + public long enabled_time; + public long last_update_time; + + public String name; + + public int lock_duration_seconds; + + public boolean enabled; + + public SchedulePojo schedule; + + /** + * + * @param enabledTime + * @param lastUpdateTime + * @param name + * @param lockDurationSeconds + * @param enabled + * @param schedule + */ + public GreetJobPojo( + long enabledTime, + long lastUpdateTime, + String name, + int lockDurationSeconds, + boolean enabled, + SchedulePojo schedule + ) { + this.enabled_time = enabledTime; + this.last_update_time = lastUpdateTime; + this.name = name; + this.lock_duration_seconds = lockDurationSeconds; + this.enabled = enabled; + this.schedule = schedule; + } + + /** + * A plain java representation of a Schedule using only primitives + */ + public static class SchedulePojo { + + public IntervalPojo interval; + + /** + * + * @param interval An Interval instance + */ + public SchedulePojo(IntervalPojo interval) { + this.interval = interval; + } + + /** + * A plain java representation of a Interval using only primitives + */ + public static class IntervalPojo { + public String unit; + + public int period; + + public long start_time; + + /** + * + * @param unit Unit of time + * @param period Number of units between job execution + * @param start_time The time when the interval first started + */ + public IntervalPojo(String unit, int period, long start_time) { + this.unit = unit; + this.period = period; + this.start_time = start_time; + } + } + } + } +} diff --git a/sample/src/main/java/org/opensearch/sdk/sample/helloworld/spec/openapi.json b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/spec/openapi.json new file mode 100644 index 00000000..85c7da69 --- /dev/null +++ b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/spec/openapi.json @@ -0,0 +1,149 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Hello World", + "description": "This is a sample Hello World extension.", + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0.0-SNAPSHOT" + }, + "tags": [ + { + "name": "hello", + "description": "Worldly Greetings" + } + ], + "paths": { + "/hello": { + "get": { + "tags": [ + "hello" + ], + "summary": "Greet the world", + "description": "Traditional greeting", + "responses": { + "200": { + "description": "Successful operation", + "content": { + "text/plain; charset=utf-8": { + "examples": { + "Default Response": { + "value": "Hello, World!" + } + } + } + } + }, + "400": { + "description": "Syntax Error in URI" + }, + "404": { + "description": "Improper REST action configuration" + } + } + }, + "post": { + "tags": [ + "hello" + ], + "summary": "Adds a descriptive world adjective to a list", + "description": "Adds an adjective to a list from which a random element will be prepended to the world name", + "operationId": "", + "requestBody": { + "description": "An adjective in plain text or JSON", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "adjective": { + "type": "string", + "example": "wonderful" + } + } + } + }, + "text/plain": { + "schema": { + "type": "string", + "example": "wonderful" + } + } + } + }, + "responses": { + "200": { + "description": "Successful operation" + }, + "400": { + "description": "Syntax Error in request" + }, + "404": { + "description": "Improper REST action configuration" + }, + "406": { + "description": "Content format not text or JSON" + } + } + } + }, + "/hello/{name}": { + "put": { + "tags": [ + "hello" + ], + "summary": "Rename the world", + "description": "Update the world to a custom name", + "parameters": [ + { + "name": "name", + "in": "path", + "description": "A new name for the world", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successful operation", + "content": { + "text/plain; charset=utf-8": { + "examples": { + "Default Response": { + "value": "Updated the world's name to OpenSearch" + } + } + } + } + }, + "400": { + "description": "Syntax Error in URI" + }, + "404": { + "description": "Improper REST action configuration" + } + } + } + }, + "/goodbye": { + "delete": { + "tags": [ + "hello" + ], + "summary": "Restores the world to default", + "description": "Removes all adjectives and the custom world name", + "operationId": "", + "responses": { + "200": { + "description": "Successful operation" + } + } + } + } + } +} diff --git a/sample/src/main/java/org/opensearch/sdk/sample/helloworld/spec/openapi.yaml b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/spec/openapi.yaml new file mode 100644 index 00000000..b8ffc77b --- /dev/null +++ b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/spec/openapi.yaml @@ -0,0 +1,117 @@ +openapi: 3.0.3 +info: + title: Hello World + description: This is a sample Hello World extension. + license: + name: Apache 2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html + version: 1.0.0-SNAPSHOT +tags: + - name: hello + description: Worldly Greetings +paths: + /hello: + get: + tags: + - hello + summary: Greet the world + description: Traditional greeting + responses: + '200': + description: Successful operation + content: + text/plain; charset=utf-8: + examples: + Default Response: + value: Hello, World! + '400': + description: Syntax Error in URI + '404': + description: Improper REST action configuration + post: + tags: + - hello + summary: Adds a descriptive world adjective to a list + description: >- + Adds an adjective to a list from which a random element will be + prepended to the world name + operationId: '' + requestBody: + description: An adjective in plain text or JSON + required: true + content: + application/json: + schema: + type: object + properties: + adjective: + type: string + example: wonderful + text/plain: + schema: + type: string + example: wonderful + responses: + '200': + description: Successful operation + '400': + description: Syntax Error in request + '404': + description: Improper REST action configuration + '406': + description: Content format not text or JSON + delete: + tags: + - hello + summary: Removes an adjective from the list + description: >- + Removes an adjective from the list from which a random element + will be prepended to the world name + operationId: '' + responses: + '200': + description: Successful operation + '304': + description: Adjective not in the list, no action taken + '400': + description: Syntax Error in request + '404': + description: Improper REST action configuration + '406': + description: Content format not text or JSON + /hello/{name}: + put: + tags: + - hello + summary: Rename the world + description: Update the world to a custom name + parameters: + - name: name + in: path + description: A new name for the world + required: true + schema: + type: string + responses: + '200': + description: Successful operation + content: + text/plain; charset=utf-8: + examples: + Default Response: + value: Updated the world's name to OpenSearch + '400': + description: Syntax Error in URI + '404': + description: Improper REST action configuration + /goodbye: + delete: + tags: + - hello + summary: Restores the world to default + description: >- + Removes all adjectives and the custom world name + operationId: '' + responses: + '200': + description: Successful operation diff --git a/sample/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobParameterAction.java b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobParameterAction.java new file mode 100644 index 00000000..78d09714 --- /dev/null +++ b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobParameterAction.java @@ -0,0 +1,25 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.sdk.sample.helloworld.transport; + +import org.opensearch.action.ActionType; +import org.opensearch.jobscheduler.transport.response.JobParameterResponse; + +/** + * Hello World Job Parameter Action + */ +public class HWJobParameterAction extends ActionType { + public static final String NAME = "extensions:hw/greet_job_parameter"; + public static final HWJobParameterAction INSTANCE = new HWJobParameterAction(); + + private HWJobParameterAction() { + super(NAME, JobParameterResponse::new); + } +} diff --git a/sample/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobParameterTransportAction.java b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobParameterTransportAction.java new file mode 100644 index 00000000..e5b05243 --- /dev/null +++ b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobParameterTransportAction.java @@ -0,0 +1,81 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.sdk.sample.helloworld.transport; + +import com.google.inject.Inject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.action.ActionListener; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.TransportAction; +import org.opensearch.common.xcontent.LoggingDeprecationHandler; +import org.opensearch.common.xcontent.XContentHelper; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.jobscheduler.model.ExtensionJobParameter; +import org.opensearch.jobscheduler.spi.ScheduledJobParameter; +import org.opensearch.jobscheduler.transport.request.JobParameterRequest; +import org.opensearch.jobscheduler.transport.response.JobParameterResponse; +import org.opensearch.sdk.SDKNamedXContentRegistry; +import org.opensearch.sdk.sample.helloworld.schedule.GreetJob; +import org.opensearch.tasks.Task; +import org.opensearch.tasks.TaskManager; + +import static org.opensearch.common.xcontent.XContentParserUtils.ensureExpectedToken; +import static org.opensearch.sdk.sample.helloworld.util.RestHandlerUtils.wrapRestActionListener; + +/** + * Hello World Job Parameter Transport Action + */ +public class HWJobParameterTransportAction extends TransportAction { + + private static final Logger LOG = LogManager.getLogger(HWJobParameterTransportAction.class); + + private final SDKNamedXContentRegistry xContentRegistry; + + /** + * Instantiate this action + * + * @param actionFilters Action filters + * @param taskManager The task manager + * @param xContentRegistry The xContentRegistry + */ + @Inject + protected HWJobParameterTransportAction( + ActionFilters actionFilters, + TaskManager taskManager, + SDKNamedXContentRegistry xContentRegistry + ) { + super(HWJobParameterAction.NAME, actionFilters, taskManager); + this.xContentRegistry = xContentRegistry; + } + + @Override + protected void doExecute(Task task, JobParameterRequest request, ActionListener actionListener) { + + String errorMessage = "Failed to parse the Job Parameter"; + ActionListener listener = wrapRestActionListener(actionListener, errorMessage); + try { + XContentParser parser = XContentHelper.createParser( + xContentRegistry.getRegistry(), + LoggingDeprecationHandler.INSTANCE, + request.getJobSource(), + XContentType.JSON + ); + ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser); + ScheduledJobParameter scheduledJobParameter = GreetJob.parse(parser); + JobParameterResponse jobParameterResponse = new JobParameterResponse(new ExtensionJobParameter(scheduledJobParameter)); + listener.onResponse(jobParameterResponse); + } catch (Exception e) { + LOG.error(e); + listener.onFailure(e); + } + } +} diff --git a/sample/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobRunnerAction.java b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobRunnerAction.java new file mode 100644 index 00000000..d9c41e97 --- /dev/null +++ b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobRunnerAction.java @@ -0,0 +1,26 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.sdk.sample.helloworld.transport; + +import org.opensearch.action.ActionType; +import org.opensearch.jobscheduler.transport.response.JobRunnerResponse; + +/** + * Hello World Job Runner Action + */ +public class HWJobRunnerAction extends ActionType { + + public static final String NAME = "extensions:hw/greet_job_runner"; + public static final HWJobRunnerAction INSTANCE = new HWJobRunnerAction(); + + private HWJobRunnerAction() { + super(NAME, JobRunnerResponse::new); + } +} diff --git a/sample/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobRunnerTransportAction.java b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobRunnerTransportAction.java new file mode 100644 index 00000000..bc3f290c --- /dev/null +++ b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobRunnerTransportAction.java @@ -0,0 +1,164 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.sdk.sample.helloworld.transport; + +import com.google.inject.Inject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.action.ActionListener; +import org.opensearch.action.get.GetRequest; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.TransportAction; +import org.opensearch.common.xcontent.LoggingDeprecationHandler; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.jobscheduler.spi.JobExecutionContext; +import org.opensearch.jobscheduler.transport.request.JobRunnerRequest; +import org.opensearch.jobscheduler.transport.response.JobRunnerResponse; +import org.opensearch.sdk.SDKClient.SDKRestClient; +import org.opensearch.sdk.SDKNamedXContentRegistry; +import org.opensearch.sdk.sample.helloworld.schedule.GreetJob; +import org.opensearch.tasks.Task; +import org.opensearch.tasks.TaskManager; + +import java.io.IOException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static org.opensearch.common.xcontent.XContentParserUtils.ensureExpectedToken; +import static org.opensearch.sdk.sample.helloworld.util.RestHandlerUtils.wrapRestActionListener; + +/** + * Hello World Job Runner Transport Action + */ +public class HWJobRunnerTransportAction extends TransportAction { + + private static final Logger LOG = LogManager.getLogger(HWJobRunnerTransportAction.class); + + private SDKRestClient client; + private final SDKNamedXContentRegistry xContentRegistry; + + /** + * Instantiate this action + * + * @param actionFilters Action filters + * @param taskManager The task manager + * @param xContentRegistry xContentRegistry + * @param client SDKRestClient + */ + @Inject + protected HWJobRunnerTransportAction( + ActionFilters actionFilters, + TaskManager taskManager, + SDKNamedXContentRegistry xContentRegistry, + SDKRestClient client + ) { + super(HWJobRunnerAction.NAME, actionFilters, taskManager); + this.client = client; + this.xContentRegistry = xContentRegistry; + } + + @Override + protected void doExecute(Task task, JobRunnerRequest request, ActionListener actionListener) { + String errorMessage = "Failed to run the Job"; + ActionListener listener = wrapRestActionListener(actionListener, errorMessage); + try { + JobExecutionContext jobExecutionContext = request.getJobExecutionContext(); + String jobParameterDocumentId = jobExecutionContext.getJobId(); + if (jobParameterDocumentId == null || jobParameterDocumentId.isEmpty()) { + listener.onFailure(new IllegalArgumentException("jobParameterDocumentId cannot be empty or null")); + } else { + CompletableFuture inProgressFuture = new CompletableFuture<>(); + findById(jobParameterDocumentId, new ActionListener<>() { + @Override + public void onResponse(GreetJob anomalyDetectorJob) { + inProgressFuture.complete(anomalyDetectorJob); + } + + @Override + public void onFailure(Exception e) { + logger.info("could not find GreetJob with id " + jobParameterDocumentId, e); + inProgressFuture.completeExceptionally(e); + } + }); + + try { + GreetJob scheduledJobParameter = inProgressFuture.orTimeout(10000, TimeUnit.MILLISECONDS).join(); + + JobRunnerResponse jobRunnerResponse; + if (scheduledJobParameter != null && validateJobExecutionContext(jobExecutionContext)) { + jobRunnerResponse = new JobRunnerResponse(true); + } else { + jobRunnerResponse = new JobRunnerResponse(false); + } + listener.onResponse(jobRunnerResponse); + if (jobRunnerResponse.getJobRunnerStatus()) { + HelloWorldJobRunner.getJobRunnerInstance().runJob(scheduledJobParameter, jobExecutionContext); + } + } catch (CompletionException e) { + if (e.getCause() instanceof TimeoutException) { + logger.info(" Request timed out with an exception ", e); + } else { + throw e; + } + } catch (Exception e) { + logger.info(" Could not find Job Parameter due to exception ", e); + } + + } + } catch (Exception e) { + LOG.error(e); + listener.onFailure(e); + } + } + + private void findById(String jobParameterId, ActionListener listener) { + GetRequest getRequest = new GetRequest(GreetJob.HELLO_WORLD_JOB_INDEX, jobParameterId); + try { + client.get(getRequest, ActionListener.wrap(response -> { + if (!response.isExists()) { + listener.onResponse(null); + } else { + try { + XContentParser parser = XContentType.JSON.xContent() + .createParser(xContentRegistry.getRegistry(), LoggingDeprecationHandler.INSTANCE, response.getSourceAsString()); + ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser); + listener.onResponse(GreetJob.parse(parser)); + } catch (IOException e) { + logger.error("IOException occurred finding GreetJob for jobParameterId " + jobParameterId, e); + listener.onFailure(e); + } + } + }, exception -> { + logger.error("Exception occurred finding GreetJob for jobParameterId " + jobParameterId, exception); + listener.onFailure(exception); + })); + } catch (Exception e) { + logger.error("Error occurred finding greet job with jobParameterId " + jobParameterId, e); + listener.onFailure(e); + } + + } + + private boolean validateJobExecutionContext(JobExecutionContext jobExecutionContext) { + if (jobExecutionContext != null + && jobExecutionContext.getJobId() != null + && !jobExecutionContext.getJobId().isEmpty() + && jobExecutionContext.getJobIndexName() != null + && !jobExecutionContext.getJobIndexName().isEmpty() + && jobExecutionContext.getExpectedExecutionTime() != null + && jobExecutionContext.getJobVersion() != null) { + return true; + } + return false; + } +} diff --git a/sample/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HelloWorldJobRunner.java b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HelloWorldJobRunner.java new file mode 100644 index 00000000..a46965bd --- /dev/null +++ b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HelloWorldJobRunner.java @@ -0,0 +1,47 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.sdk.sample.helloworld.transport; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.jobscheduler.spi.JobExecutionContext; +import org.opensearch.jobscheduler.spi.ScheduledJobParameter; +import org.opensearch.jobscheduler.spi.ScheduledJobRunner; + +/** + * Hello World Job Runner + */ +public class HelloWorldJobRunner implements ScheduledJobRunner { + private static final Logger log = LogManager.getLogger(HelloWorldJobRunner.class); + + private static HelloWorldJobRunner INSTANCE; + + /** + * + * @return Return or create an instance of this job runner + */ + public static HelloWorldJobRunner getJobRunnerInstance() { + if (INSTANCE != null) { + return INSTANCE; + } + synchronized (HelloWorldJobRunner.class) { + if (INSTANCE != null) { + return INSTANCE; + } + INSTANCE = new HelloWorldJobRunner(); + return INSTANCE; + } + } + + @Override + public void runJob(ScheduledJobParameter job, JobExecutionContext context) { + System.out.println("Hello, world!"); + } +} diff --git a/sample/src/main/java/org/opensearch/sdk/sample/helloworld/transport/SampleAction.java b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/transport/SampleAction.java new file mode 100644 index 00000000..0c125b73 --- /dev/null +++ b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/transport/SampleAction.java @@ -0,0 +1,32 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.sdk.sample.helloworld.transport; + +import org.opensearch.action.ActionType; +import org.opensearch.sdk.sample.helloworld.transport.SampleResponse; + +/** + * A sample {@link ActionType} used as they key for the action map + */ +public class SampleAction extends ActionType { + + /** + * The name to look up this action with + */ + public static final String NAME = "helloworld/sample"; + /** + * The singleton instance of this class + */ + public static final SampleAction INSTANCE = new SampleAction(); + + private SampleAction() { + super(NAME, SampleResponse::new); + } +} diff --git a/sample/src/main/java/org/opensearch/sdk/sample/helloworld/transport/SampleRequest.java b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/transport/SampleRequest.java new file mode 100644 index 00000000..38f1eade --- /dev/null +++ b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/transport/SampleRequest.java @@ -0,0 +1,58 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.sdk.sample.helloworld.transport; + +import java.io.IOException; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.common.io.stream.StreamInput; +import org.opensearch.common.io.stream.StreamOutput; + +/** + * A sample request class to demonstrate extension actions + */ +public class SampleRequest extends ActionRequest { + + private final String name; + + /** + * Instantiate this request + * + * @param name A name to pass to the action + */ + public SampleRequest(String name) { + this.name = name; + } + + /** + * Instantiate this request from a byte stream + * + * @param in the byte stream + * @throws IOException on failure reading the stream + */ + public SampleRequest(StreamInput in) throws IOException { + this.name = in.readString(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(name); + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + public String getName() { + return name; + } +} diff --git a/sample/src/main/java/org/opensearch/sdk/sample/helloworld/transport/SampleResponse.java b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/transport/SampleResponse.java new file mode 100644 index 00000000..80e60706 --- /dev/null +++ b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/transport/SampleResponse.java @@ -0,0 +1,52 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.sdk.sample.helloworld.transport; + +import java.io.IOException; + +import org.opensearch.action.ActionResponse; +import org.opensearch.common.io.stream.StreamInput; +import org.opensearch.common.io.stream.StreamOutput; + +/** + * A sample response class to demonstrate extension actions + */ +public class SampleResponse extends ActionResponse { + + private final String greeting; + + /** + * Instantiate this response + * + * @param greeting The greeting to return + */ + public SampleResponse(String greeting) { + this.greeting = greeting; + } + + /** + * Instantiate this response from a byte stream + * + * @param in the byte stream + * @throws IOException on failure reading the stream + */ + public SampleResponse(StreamInput in) throws IOException { + this.greeting = in.readString(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(greeting); + } + + public String getGreeting() { + return greeting; + } +} diff --git a/sample/src/main/java/org/opensearch/sdk/sample/helloworld/transport/SampleTransportAction.java b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/transport/SampleTransportAction.java new file mode 100644 index 00000000..2c80f1cf --- /dev/null +++ b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/transport/SampleTransportAction.java @@ -0,0 +1,46 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.sdk.sample.helloworld.transport; + +import org.opensearch.action.ActionListener; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.TransportAction; +import org.opensearch.tasks.Task; +import org.opensearch.tasks.TaskManager; + +import com.google.inject.Inject; + +/** + * A sample {@link TransportAction} used as they value for the action map + */ +public class SampleTransportAction extends TransportAction { + + /** + * Instantiate this action + * + * @param actionName The action name + * @param actionFilters Action filters + * @param taskManager The task manager + */ + @Inject + protected SampleTransportAction(String actionName, ActionFilters actionFilters, TaskManager taskManager) { + super(actionName, actionFilters, taskManager); + } + + @Override + protected void doExecute(Task task, SampleRequest request, ActionListener listener) { + // Fail if name is empty + if (request.getName().isBlank()) { + listener.onFailure(new IllegalArgumentException("The request name is blank.")); + } else { + listener.onResponse(new SampleResponse("Hello, " + request.getName())); + } + } +} diff --git a/sample/src/main/java/org/opensearch/sdk/sample/helloworld/util/RestHandlerUtils.java b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/util/RestHandlerUtils.java new file mode 100644 index 00000000..84172973 --- /dev/null +++ b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/util/RestHandlerUtils.java @@ -0,0 +1,91 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.sdk.sample.helloworld.util; + +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableMap; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.OpenSearchStatusException; +import org.opensearch.action.ActionListener; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.index.IndexNotFoundException; +import org.opensearch.indices.InvalidIndexNameException; +import org.opensearch.rest.RestStatus; + +import static org.opensearch.rest.RestStatus.BAD_REQUEST; +import static org.opensearch.rest.RestStatus.INTERNAL_SERVER_ERROR; + +/** + * Utility functions for REST handlers. + */ +public final class RestHandlerUtils { + private static final Logger logger = LogManager.getLogger(RestHandlerUtils.class); + + public static final ToXContent.MapParams XCONTENT_WITH_TYPE = new ToXContent.MapParams(ImmutableMap.of("with_type", "true")); + + private RestHandlerUtils() {} + + /** + * Wrap action listener to avoid return verbose error message and wrong 500 error to user. + * Suggestion for exception handling in HW: + * 1. If the error is caused by wrong input, throw IllegalArgumentException exception. + * 2. For other errors, please use OpenSearchStatusException. + * + * TODO: tune this function for wrapped exception, return root exception error message + * + * @param actionListener action listener + * @param generalErrorMessage general error message + * @param action listener response type + * @return wrapped action listener + */ + public static ActionListener wrapRestActionListener(ActionListener actionListener, String generalErrorMessage) { + return ActionListener.wrap(r -> { actionListener.onResponse(r); }, e -> { + logger.error("Wrap exception before sending back to user", e); + Throwable cause = Throwables.getRootCause(e); + if (isProperExceptionToReturn(e)) { + actionListener.onFailure(e); + } else if (isProperExceptionToReturn(cause)) { + actionListener.onFailure((Exception) cause); + } else { + RestStatus status = isBadRequest(e) ? BAD_REQUEST : INTERNAL_SERVER_ERROR; + String errorMessage = generalErrorMessage; + if (isBadRequest(e)) { + errorMessage = e.getMessage(); + } else if (cause != null && isBadRequest(cause)) { + errorMessage = cause.getMessage(); + } + actionListener.onFailure(new OpenSearchStatusException(errorMessage, status)); + } + }); + } + + /** + * @param e Throwable + * @return Return a boolean whether there was an exception thrown on a request + */ + public static boolean isBadRequest(Throwable e) { + if (e == null) { + return false; + } + return e instanceof IllegalArgumentException; + } + + /** + * @param e Throwable + * @return Return boolean if throwable is of a proper exception type + */ + public static boolean isProperExceptionToReturn(Throwable e) { + if (e == null) { + return false; + } + return e instanceof OpenSearchStatusException || e instanceof IndexNotFoundException || e instanceof InvalidIndexNameException; + } +} diff --git a/sample/src/main/resources/mappings/hello-world-jobs.json b/sample/src/main/resources/mappings/hello-world-jobs.json new file mode 100644 index 00000000..43cfec8c --- /dev/null +++ b/sample/src/main/resources/mappings/hello-world-jobs.json @@ -0,0 +1,50 @@ +{ + ".hello-world-jobs": { + "mappings": { + "properties": { + "schema_version": { + "type": "integer" + }, + "name": { + "type": "keyword" + }, + "schedule": { + "properties": { + "interval": { + "properties": { + "start_time": { + "type": "date", + "format": "strict_date_time||epoch_millis" + }, + "period": { + "type": "integer" + }, + "unit": { + "type": "keyword" + } + } + } + } + }, + "enabled": { + "type": "boolean" + }, + "enabled_time": { + "type": "date", + "format": "strict_date_time||epoch_millis" + }, + "disabled_time": { + "type": "date", + "format": "strict_date_time||epoch_millis" + }, + "last_update_time": { + "type": "date", + "format": "strict_date_time||epoch_millis" + }, + "lock_duration_seconds": { + "type": "long" + } + } + } + } +} diff --git a/sample/src/main/resources/sample/helloworld-settings.yml b/sample/src/main/resources/sample/helloworld-settings.yml new file mode 100644 index 00000000..417136e2 --- /dev/null +++ b/sample/src/main/resources/sample/helloworld-settings.yml @@ -0,0 +1,5 @@ +extensionName: hello-world +hostAddress: 127.0.0.1 +hostPort: 4532 +opensearchAddress: 127.0.0.1 +opensearchPort: 9200 diff --git a/settings.gradle b/settings.gradle index c15ed9a9..90b24732 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,5 @@ rootProject.name = 'opensearch-sdk-java' + + +include "sample" +project(":sample").name = rootProject.name + "-sample-extension" From 60ce094aec9fb7fb2c3c777526e0963376c13243 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Mon, 1 May 2023 16:44:44 -0400 Subject: [PATCH 07/16] WIP on multi-project Signed-off-by: Craig Perkins --- build.gradle | 1 - sample/build.gradle | 21 +- .../helloworld/TestHelloWorldExtension.java | 0 .../helloworld/rest/TestRestHelloAction.java | 0 .../transport/TestSampleAction.java | 0 .../ExampleCustomSettingConfig.java | 30 -- .../helloworld/HelloWorldExtension.java | 117 ------ .../helloworld/rest/RestHelloAction.java | 133 ------ .../rest/RestRemoteHelloAction.java | 263 ------------ .../sample/helloworld/schedule/GreetJob.java | 380 ------------------ .../sdk/sample/helloworld/spec/openapi.json | 149 ------- .../sdk/sample/helloworld/spec/openapi.yaml | 117 ------ .../transport/HWJobParameterAction.java | 25 -- .../HWJobParameterTransportAction.java | 81 ---- .../transport/HWJobRunnerAction.java | 26 -- .../transport/HWJobRunnerTransportAction.java | 164 -------- .../transport/HelloWorldJobRunner.java | 47 --- .../helloworld/transport/SampleAction.java | 31 -- .../helloworld/transport/SampleRequest.java | 58 --- .../helloworld/transport/SampleResponse.java | 52 --- .../transport/SampleTransportAction.java | 46 --- .../helloworld/util/RestHandlerUtils.java | 91 ----- 22 files changed, 16 insertions(+), 1816 deletions(-) rename {src => sample/src}/test/java/org/opensearch/sdk/sample/helloworld/TestHelloWorldExtension.java (100%) rename {src => sample/src}/test/java/org/opensearch/sdk/sample/helloworld/rest/TestRestHelloAction.java (100%) rename {src => sample/src}/test/java/org/opensearch/sdk/sample/helloworld/transport/TestSampleAction.java (100%) delete mode 100644 src/main/java/org/opensearch/sdk/sample/helloworld/ExampleCustomSettingConfig.java delete mode 100644 src/main/java/org/opensearch/sdk/sample/helloworld/HelloWorldExtension.java delete mode 100644 src/main/java/org/opensearch/sdk/sample/helloworld/rest/RestHelloAction.java delete mode 100644 src/main/java/org/opensearch/sdk/sample/helloworld/rest/RestRemoteHelloAction.java delete mode 100644 src/main/java/org/opensearch/sdk/sample/helloworld/schedule/GreetJob.java delete mode 100644 src/main/java/org/opensearch/sdk/sample/helloworld/spec/openapi.json delete mode 100644 src/main/java/org/opensearch/sdk/sample/helloworld/spec/openapi.yaml delete mode 100644 src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobParameterAction.java delete mode 100644 src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobParameterTransportAction.java delete mode 100644 src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobRunnerAction.java delete mode 100644 src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobRunnerTransportAction.java delete mode 100644 src/main/java/org/opensearch/sdk/sample/helloworld/transport/HelloWorldJobRunner.java delete mode 100644 src/main/java/org/opensearch/sdk/sample/helloworld/transport/SampleAction.java delete mode 100644 src/main/java/org/opensearch/sdk/sample/helloworld/transport/SampleRequest.java delete mode 100644 src/main/java/org/opensearch/sdk/sample/helloworld/transport/SampleResponse.java delete mode 100644 src/main/java/org/opensearch/sdk/sample/helloworld/transport/SampleTransportAction.java delete mode 100644 src/main/java/org/opensearch/sdk/sample/helloworld/util/RestHandlerUtils.java diff --git a/build.gradle b/build.gradle index b334d6e4..0a3d5e9b 100644 --- a/build.gradle +++ b/build.gradle @@ -84,7 +84,6 @@ subprojects { dependencies { def opensearchVersion = "3.0.0-SNAPSHOT" - def jobSchedulerVersion = "3.0.0.0-SNAPSHOT" def log4jVersion = "2.20.0" def nettyVersion = "4.1.92.Final" def jacksonDatabindVersion = "2.15.0" diff --git a/sample/build.gradle b/sample/build.gradle index 7b547ef8..44188a73 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -1,3 +1,5 @@ +import org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestFramework + /* * SPDX-License-Identifier: Apache-2.0 * @@ -9,8 +11,6 @@ * GitHub history for details. */ -import java.nio.file.Files - plugins { id 'java' id "com.diffplug.spotless" version "6.18.0" apply false @@ -20,7 +20,6 @@ plugins { id 'checkstyle' } - ext { projectSubstitutions = [:] licenseFile = rootProject.file('LICENSE.txt') @@ -68,13 +67,25 @@ publishing { } } +test { + systemProperty 'tests.security.manager', 'false' +} + dependencies { def opensearchVersion = "3.0.0-SNAPSHOT" def jobSchedulerVersion = "3.0.0.0-SNAPSHOT" + def junit5Version = "5.9.3" + def junitPlatform = "1.9.3" + implementation project(':') implementation("org.opensearch.plugin:opensearch-job-scheduler:${jobSchedulerVersion}") - implementation("org.opensearch:opensearch-job-scheduler:${jobSchedulerVersion}") - implementation "org.opensearch:opensearch-job-scheduler-spi:${jobSchedulerVersion}" + implementation("org.opensearch:opensearch-job-scheduler-spi:${jobSchedulerVersion}") + + testImplementation project(':').sourceSets.test.output + testImplementation("org.junit.jupiter:junit-jupiter-api:${junit5Version}") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${junit5Version}") + testImplementation("org.opensearch.test:framework:${opensearchVersion}") + testRuntimeOnly("org.junit.platform:junit-platform-launcher:${junitPlatform}") } // this task runs the helloworld sample extension diff --git a/src/test/java/org/opensearch/sdk/sample/helloworld/TestHelloWorldExtension.java b/sample/src/test/java/org/opensearch/sdk/sample/helloworld/TestHelloWorldExtension.java similarity index 100% rename from src/test/java/org/opensearch/sdk/sample/helloworld/TestHelloWorldExtension.java rename to sample/src/test/java/org/opensearch/sdk/sample/helloworld/TestHelloWorldExtension.java diff --git a/src/test/java/org/opensearch/sdk/sample/helloworld/rest/TestRestHelloAction.java b/sample/src/test/java/org/opensearch/sdk/sample/helloworld/rest/TestRestHelloAction.java similarity index 100% rename from src/test/java/org/opensearch/sdk/sample/helloworld/rest/TestRestHelloAction.java rename to sample/src/test/java/org/opensearch/sdk/sample/helloworld/rest/TestRestHelloAction.java diff --git a/src/test/java/org/opensearch/sdk/sample/helloworld/transport/TestSampleAction.java b/sample/src/test/java/org/opensearch/sdk/sample/helloworld/transport/TestSampleAction.java similarity index 100% rename from src/test/java/org/opensearch/sdk/sample/helloworld/transport/TestSampleAction.java rename to sample/src/test/java/org/opensearch/sdk/sample/helloworld/transport/TestSampleAction.java diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/ExampleCustomSettingConfig.java b/src/main/java/org/opensearch/sdk/sample/helloworld/ExampleCustomSettingConfig.java deleted file mode 100644 index b5c7e38e..00000000 --- a/src/main/java/org/opensearch/sdk/sample/helloworld/ExampleCustomSettingConfig.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sdk.sample.helloworld; - -import org.opensearch.common.settings.Setting; -import org.opensearch.common.settings.Setting.RegexValidator; -import org.opensearch.common.settings.Setting.Property; - -/** - * {@link ExampleCustomSettingConfig} contains the custom settings value and their static declarations. - */ -public class ExampleCustomSettingConfig { - private static final String FORBIDDEN_VALUE = "forbidden"; - /** - * A string setting. If the string setting matches the FORBIDDEN_REGEX string and parameter isMatching is true the validation will fail. - */ - public static final Setting VALIDATED_SETTING = Setting.simpleString( - "custom.validate", - new RegexValidator(FORBIDDEN_VALUE, false), - Property.NodeScope, - Property.Dynamic - ); -} diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/HelloWorldExtension.java b/src/main/java/org/opensearch/sdk/sample/helloworld/HelloWorldExtension.java deleted file mode 100644 index c4fc8ba3..00000000 --- a/src/main/java/org/opensearch/sdk/sample/helloworld/HelloWorldExtension.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sdk.sample.helloworld; - -import java.io.IOException; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.List; - -import com.google.common.collect.ImmutableList; -import org.opensearch.action.ActionRequest; -import org.opensearch.action.ActionResponse; -import org.opensearch.common.settings.Setting; -import org.opensearch.core.xcontent.NamedXContentRegistry; -import org.opensearch.sdk.BaseExtension; -import org.opensearch.sdk.Extension; -import org.opensearch.sdk.ExtensionSettings; -import org.opensearch.sdk.ExtensionsRunner; -import org.opensearch.sdk.SDKClient; -import org.opensearch.sdk.api.ActionExtension; -import org.opensearch.sdk.rest.ExtensionRestHandler; -import org.opensearch.sdk.sample.helloworld.rest.RestHelloAction; -import org.opensearch.sdk.sample.helloworld.rest.RestRemoteHelloAction; -import org.opensearch.sdk.sample.helloworld.schedule.GreetJob; -import org.opensearch.sdk.sample.helloworld.transport.HWJobParameterAction; -import org.opensearch.sdk.sample.helloworld.transport.HWJobParameterTransportAction; -import org.opensearch.sdk.sample.helloworld.transport.HWJobRunnerAction; -import org.opensearch.sdk.sample.helloworld.transport.HWJobRunnerTransportAction; -import org.opensearch.sdk.sample.helloworld.transport.SampleAction; -import org.opensearch.sdk.sample.helloworld.transport.SampleTransportAction; - -/** - * Sample class to demonstrate how to use the OpenSearch SDK for Java to create - * an extension. - *

- * To create your own extension, implement the {@link #getExtensionSettings()} and {@link #getExtensionRestHandlers()} methods. - * You may either create an {@link ExtensionSettings} object directly with the constructor, or read it from a YAML file on your class path. - *

- * To execute, pass an instatiated object of this class to {@link ExtensionsRunner#run(Extension)}. - */ -public class HelloWorldExtension extends BaseExtension implements ActionExtension { - - /** - * Optional classpath-relative path to a yml file containing extension settings. - */ - private static final String EXTENSION_SETTINGS_PATH = "/sample/helloworld-settings.yml"; - - /** - * Instantiate this extension, initializing the connection settings and REST actions. - * The Extension must provide its settings to the ExtensionsRunner. - * These may be optionally read from a YAML file on the class path. - * Or you may directly instantiate with the ExtensionSettings constructor. - * - */ - public HelloWorldExtension() { - super(EXTENSION_SETTINGS_PATH); - } - - @Override - public List getExtensionRestHandlers() { - return List.of(new RestHelloAction(), new RestRemoteHelloAction(extensionsRunner())); - } - - @Override - public List> getActions() { - return Arrays.asList( - new ActionHandler<>(SampleAction.INSTANCE, SampleTransportAction.class), - new ActionHandler<>(HWJobRunnerAction.INSTANCE, HWJobRunnerTransportAction.class), - new ActionHandler<>(HWJobParameterAction.INSTANCE, HWJobParameterTransportAction.class) - ); - } - - @Override - public List getNamedXContent() { - return ImmutableList.of(GreetJob.XCONTENT_REGISTRY); - } - - @Deprecated - private SDKClient.SDKRestClient createRestClient(ExtensionsRunner runner) { - @SuppressWarnings("resource") - SDKClient.SDKRestClient client = runner.getSdkClient().initializeRestClient(); - return client; - } - - @Override - public Collection createComponents(ExtensionsRunner runner) { - SDKClient.SDKRestClient sdkRestClient = createRestClient(runner); - - return Collections.singletonList(sdkRestClient); - } - - /** - * A list of object that includes a single instance of Validator for Custom Setting - */ - public List> getSettings() { - return Arrays.asList(ExampleCustomSettingConfig.VALIDATED_SETTING); - } - - /** - * Entry point to execute an extension. - * - * @param args Unused. - * @throws IOException on a failure in the ExtensionsRunner - */ - public static void main(String[] args) throws IOException { - // Execute this extension by instantiating it and passing to ExtensionsRunner - ExtensionsRunner.run(new HelloWorldExtension()); - } -} diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/rest/RestHelloAction.java b/src/main/java/org/opensearch/sdk/sample/helloworld/rest/RestHelloAction.java deleted file mode 100644 index 8673d48c..00000000 --- a/src/main/java/org/opensearch/sdk/sample/helloworld/rest/RestHelloAction.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sdk.sample.helloworld.rest; - -import org.opensearch.OpenSearchParseException; -import org.opensearch.common.bytes.BytesReference; -import org.opensearch.core.xcontent.XContentBuilder; -import org.opensearch.common.xcontent.XContentType; -import org.opensearch.common.xcontent.json.JsonXContent; -import org.opensearch.extensions.rest.ExtensionRestResponse; -import org.opensearch.rest.RestRequest; -import org.opensearch.sdk.rest.BaseExtensionRestHandler; -import org.opensearch.sdk.rest.ExtensionRestHandler; -import java.io.IOException; -import java.net.URLDecoder; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; -import java.util.Random; -import java.util.function.Function; - -import static org.opensearch.rest.RestRequest.Method.DELETE; -import static org.opensearch.rest.RestRequest.Method.GET; -import static org.opensearch.rest.RestRequest.Method.POST; -import static org.opensearch.rest.RestRequest.Method.PUT; -import static org.opensearch.rest.RestStatus.BAD_REQUEST; -import static org.opensearch.rest.RestStatus.NOT_ACCEPTABLE; -import static org.opensearch.rest.RestStatus.OK; - -/** - * Sample REST Handler (REST Action). - * Extension REST handlers must implement {@link ExtensionRestHandler}. - * Extending {@link BaseExtensionRestHandler} provides many convenience methods. - */ -public class RestHelloAction extends BaseExtensionRestHandler { - - private static final String TEXT_CONTENT_TYPE = "text/plain; charset=UTF-8"; - private static final String GREETING = "Hello, %s!"; - private static final String DEFAULT_NAME = "World"; - - private String worldName = DEFAULT_NAME; - private List worldAdjectives = new ArrayList<>(); - private Random rand = new Random(); - - @Override - public List routeHandlers() { - return List.of( - new RouteHandler(GET, "/hello", handleGetRequest), - new RouteHandler(POST, "/hello", handlePostRequest), - new RouteHandler(PUT, "/hello/{name}", handlePutRequest), - new RouteHandler(DELETE, "/goodbye", handleDeleteRequest) - ); - } - - private Function handleGetRequest = (request) -> { - String worldNameWithRandomAdjective = worldAdjectives.isEmpty() - ? worldName - : String.join(" ", worldAdjectives.get(rand.nextInt(worldAdjectives.size())), worldName); - return new ExtensionRestResponse(request, OK, String.format(GREETING, worldNameWithRandomAdjective)); - }; - - private Function handlePostRequest = (request) -> { - if (request.hasContent()) { - String adjective = ""; - XContentType contentType = request.getXContentType(); - if (contentType == null) { - // Plain text - adjective = request.content().utf8ToString(); - } else if (contentType.equals(XContentType.JSON)) { - try { - adjective = request.contentParser().mapStrings().get("adjective"); - } catch (IOException | OpenSearchParseException e) { - // Sample plain text response - return new ExtensionRestResponse(request, BAD_REQUEST, "Unable to parse adjective from JSON"); - } - } else { - // Sample text response with content type - return new ExtensionRestResponse( - request, - NOT_ACCEPTABLE, - TEXT_CONTENT_TYPE, - "Only text and JSON content types are supported" - ); - } - if (adjective != null && !adjective.isBlank()) { - worldAdjectives.add(adjective.trim()); - // Sample JSON response with a builder - try { - XContentBuilder builder = JsonXContent.contentBuilder() - .startObject() - .field("worldAdjectives", worldAdjectives) - .endObject(); - return new ExtensionRestResponse(request, OK, builder); - } catch (IOException e) { - // Sample response for developer error - return unhandledRequest(request); - } - } - byte[] noAdjective = "No adjective included with POST request".getBytes(StandardCharsets.UTF_8); - // Sample binary response with content type - return new ExtensionRestResponse(request, BAD_REQUEST, TEXT_CONTENT_TYPE, noAdjective); - } - // Sample bytes reference response with content type - BytesReference noContent = BytesReference.fromByteBuffer( - ByteBuffer.wrap("No content included with POST request".getBytes(StandardCharsets.UTF_8)) - ); - return new ExtensionRestResponse(request, BAD_REQUEST, TEXT_CONTENT_TYPE, noContent); - }; - - private Function handlePutRequest = (request) -> { - String name = request.param("name"); - try { - worldName = URLDecoder.decode(name, StandardCharsets.UTF_8); - } catch (IllegalArgumentException e) { - return new ExtensionRestResponse(request, BAD_REQUEST, e.getMessage()); - } - return new ExtensionRestResponse(request, OK, "Updated the world's name to " + worldName); - }; - - private Function handleDeleteRequest = (request) -> { - this.worldName = DEFAULT_NAME; - this.worldAdjectives.clear(); - return new ExtensionRestResponse(request, OK, "Goodbye, cruel world! Restored default values."); - }; -} diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/rest/RestRemoteHelloAction.java b/src/main/java/org/opensearch/sdk/sample/helloworld/rest/RestRemoteHelloAction.java deleted file mode 100644 index be6e1220..00000000 --- a/src/main/java/org/opensearch/sdk/sample/helloworld/rest/RestRemoteHelloAction.java +++ /dev/null @@ -1,263 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sdk.sample.helloworld.rest; - -import com.google.common.io.Resources; -import jakarta.json.stream.JsonParser; -import org.apache.commons.codec.Charsets; -import org.opensearch.OpenSearchException; -import org.opensearch.ResourceAlreadyExistsException; -import org.opensearch.action.ActionListener; -import org.opensearch.client.Request; -import org.opensearch.client.Response; -import org.opensearch.client.WarningFailureException; -import org.opensearch.client.json.JsonpMapper; -import org.opensearch.client.json.jackson.JacksonJsonpMapper; -import org.opensearch.client.json.jsonb.JsonbJsonpMapper; -import org.opensearch.client.opensearch.OpenSearchClient; -import org.opensearch.client.opensearch._types.mapping.TypeMapping; -import org.opensearch.client.opensearch.core.IndexRequest; -import org.opensearch.client.opensearch.indices.CreateIndexRequest; -import org.opensearch.client.opensearch.indices.GetMappingResponse; -import org.opensearch.client.opensearch.indices.OpenSearchIndicesClient; -import org.opensearch.common.Strings; -import org.opensearch.common.io.stream.StreamInput; -import org.opensearch.common.xcontent.json.JsonXContent; -import org.opensearch.core.xcontent.XContentBuilder; -import org.opensearch.extensions.ExtensionsManager; -import org.opensearch.extensions.action.RemoteExtensionActionResponse; -import org.opensearch.extensions.rest.ExtensionRestResponse; -import org.opensearch.jobscheduler.JobSchedulerPlugin; -import org.opensearch.jobscheduler.rest.request.GetJobDetailsRequest; -import org.opensearch.jobscheduler.spi.schedule.IntervalSchedule; -import org.opensearch.jobscheduler.spi.schedule.Schedule; -import org.opensearch.rest.RestRequest; -import org.opensearch.rest.RestStatus; -import org.opensearch.sdk.ExtensionsRunner; -import org.opensearch.sdk.SDKClient; -import org.opensearch.sdk.action.RemoteExtensionAction; -import org.opensearch.sdk.action.RemoteExtensionActionRequest; -import org.opensearch.sdk.rest.BaseExtensionRestHandler; -import org.opensearch.sdk.sample.helloworld.schedule.GreetJob; -import org.opensearch.sdk.sample.helloworld.transport.HWJobParameterAction; -import org.opensearch.sdk.sample.helloworld.transport.HWJobRunnerAction; -import org.opensearch.sdk.sample.helloworld.transport.SampleAction; -import org.opensearch.sdk.sample.helloworld.transport.SampleRequest; -import org.opensearch.sdk.sample.helloworld.transport.SampleResponse; - -import java.io.IOException; -import java.io.StringReader; -import java.net.URL; -import java.time.Duration; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.List; -import java.util.Locale; -import java.util.Random; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; -import java.util.function.Function; - -import static org.opensearch.rest.RestRequest.Method.GET; -import static org.opensearch.rest.RestRequest.Method.PUT; -import static org.opensearch.rest.RestStatus.OK; - -/** - * Sample REST Handler demonstrating proxy actions to another extension - */ -public class RestRemoteHelloAction extends BaseExtensionRestHandler { - private ExtensionsRunner extensionsRunner; - - /** - * Instantiate this action - * - * @param runner The ExtensionsRunner instance - */ - public RestRemoteHelloAction(ExtensionsRunner runner) { - this.extensionsRunner = runner; - } - - @Override - public List routeHandlers() { - return List.of( - new RouteHandler(GET, "/hello/{name}", handleRemoteGetRequest), - new RouteHandler(PUT, "/schedule/hello", handleScheduleRequest) - ); - } - - private void registerJobDetails(SDKClient.SDKRestClient client) throws IOException { - - XContentBuilder requestBody = JsonXContent.contentBuilder(); - requestBody.startObject(); - requestBody.field(GetJobDetailsRequest.JOB_INDEX, GreetJob.HELLO_WORLD_JOB_INDEX); - requestBody.field(GetJobDetailsRequest.JOB_TYPE, GreetJob.PARSE_FIELD_NAME); - requestBody.field(GetJobDetailsRequest.JOB_PARAMETER_ACTION, HWJobParameterAction.class.getName()); - requestBody.field(GetJobDetailsRequest.JOB_RUNNER_ACTION, HWJobRunnerAction.class.getName()); - requestBody.field(GetJobDetailsRequest.EXTENSION_UNIQUE_ID, extensionsRunner.getUniqueId()); - requestBody.endObject(); - - Request request = new Request("PUT", String.format(Locale.ROOT, "%s/%s", JobSchedulerPlugin.JS_BASE_URI, "_job_details")); - request.setJsonEntity(Strings.toString(requestBody)); - - Response response = client.performRequest(request); - boolean registeredJobDetails = RestStatus.fromCode(response.getStatusLine().getStatusCode()) == RestStatus.OK ? true : false; - } - - /** - * Get hello world job index mapping json content. - * - * @return hello world job index mapping - * @throws IOException IOException if mapping file can't be read correctly - */ - public static String getHelloWorldJobMappings() throws IOException { - URL url = RestRemoteHelloAction.class.getClassLoader().getResource("mappings/hello-world-jobs.json"); - return Resources.toString(url, Charsets.UTF_8); - } - - private JsonpMapper setupMapper(int rand) { - // Randomly choose json-b or jackson - if (rand % 2 == 0) { - return new JsonbJsonpMapper() { - @Override - public boolean ignoreUnknownFields() { - return false; - } - }; - } else { - return new JacksonJsonpMapper() { - @Override - public boolean ignoreUnknownFields() { - return false; - } - }; - } - } - - /** - * Deserializes json string into a java object - * - * @param json The JSON string - * @param clazz The class to deserialize to - * @param The class to deserialize to - * - * @return Object instance of clazz requested - */ - public T fromJson(String json, Class clazz) { - int rand = new Random().nextInt(); - JsonpMapper mapper = setupMapper(rand); - JsonParser parser = mapper.jsonProvider().createParser(new StringReader(json)); - return mapper.deserialize(parser, clazz); - } - - private Function handleScheduleRequest = (request) -> { - SDKClient client = extensionsRunner.getSdkClient(); - SDKClient.SDKRestClient restClient = client.initializeRestClient(); - OpenSearchClient javaClient = client.initializeJavaClient(); - - try { - registerJobDetails(restClient); - } catch (WarningFailureException e) { - // ignore - } catch (Exception e) { - if (e instanceof ResourceAlreadyExistsException - || e.getCause() instanceof ResourceAlreadyExistsException - || e.getMessage().contains("resource_already_exists_exception")) { - // ignore - } else { - // Catch all other OpenSearchExceptions - return new ExtensionRestResponse(request, RestStatus.INTERNAL_SERVER_ERROR, e.getMessage()); - } - } - - try { - String mappingsJson = getHelloWorldJobMappings(); - GetMappingResponse response = fromJson(mappingsJson, GetMappingResponse.class); - TypeMapping mappings = response.get(GreetJob.HELLO_WORLD_JOB_INDEX).mappings(); - - CreateIndexRequest cir = new CreateIndexRequest.Builder().index(GreetJob.HELLO_WORLD_JOB_INDEX).mappings(mappings).build(); - - OpenSearchIndicesClient indicesClient = javaClient.indices(); - indicesClient.create(cir); - } catch (IOException e) { - return new ExtensionRestResponse(request, RestStatus.INTERNAL_SERVER_ERROR, e.getMessage()); - } catch (WarningFailureException e) { - // TODO This is failing on ConvertResponse. Ignoring. - /* - * org.opensearch.transport.RemoteTransportException: [hello-world][127.0.0.1:4532][internal:extensions/restexecuteonextensiontaction] - * Caused by: org.opensearch.common.io.stream.NotSerializableExceptionWrapper: warning_failure_exception: method [PUT], host [https://127.0.0.1:9200], URI [/.hello-world-jobs], status line [HTTP/2.0 200 OK] - * Warnings: [index name [.hello-world-jobs] starts with a dot '.', in the next major version, index names starting with a dot are reserved for hidden indices and system indices, this request accesses system indices: [.opendistro_security], but in a future major version, direct access to system indices will be prevented by default] - * {"acknowledged":true,"shards_acknowledged":true,"index":".hello-world-jobs"} - */ - } catch (OpenSearchException e) { - if (e instanceof ResourceAlreadyExistsException || e.getCause() instanceof ResourceAlreadyExistsException) {} else { - // Catch all other OpenSearchExceptions - return new ExtensionRestResponse(request, RestStatus.INTERNAL_SERVER_ERROR, e.getMessage()); - } - } catch (Exception e) { - if (e.getMessage().contains("resource_already_exists_exception")) {} else { - // Catch all other OpenSearchExceptions - return new ExtensionRestResponse(request, RestStatus.INTERNAL_SERVER_ERROR, e.getMessage()); - } - } - - Schedule schedule = new IntervalSchedule(Instant.now(), 1, ChronoUnit.MINUTES); - Duration duration = Duration.of(1, ChronoUnit.MINUTES); - - GreetJob job = new GreetJob("hw", schedule, true, Instant.now(), null, Instant.now(), duration.getSeconds()); - - try { - // Reference: AnomalyDetector - IndexAnomalyDetectorJobActionHandler.indexAnomalyDetectorJob - IndexRequest ir = new IndexRequest.Builder().index(GreetJob.HELLO_WORLD_JOB_INDEX).document(job.toPojo()).build(); - javaClient.index(ir); - } catch (IOException e) { - return new ExtensionRestResponse(request, RestStatus.INTERNAL_SERVER_ERROR, e.getMessage()); - } - - return new ExtensionRestResponse(request, OK, "GreetJob successfully scheduled"); - }; - - private Function handleRemoteGetRequest = (request) -> { - SDKClient client = extensionsRunner.getSdkClient(); - - String name = request.param("name"); - // Create a request using class on remote - // This class happens to be local for simplicity but is a class on the remote extension - SampleRequest sampleRequest = new SampleRequest(name); - - // Serialize this request in a proxy action request - // This requires that the remote extension has a corresponding transport action registered - // This Action class happens to be local for simplicity but is a class on the remote extension - RemoteExtensionActionRequest proxyActionRequest = new RemoteExtensionActionRequest(SampleAction.INSTANCE, sampleRequest); - - // TODO: We need async client.execute to hide these action listener details and return the future directly - // https://github.com/opensearch-project/opensearch-sdk-java/issues/584 - CompletableFuture futureResponse = new CompletableFuture<>(); - client.execute( - RemoteExtensionAction.INSTANCE, - proxyActionRequest, - ActionListener.wrap(r -> futureResponse.complete(r), e -> futureResponse.completeExceptionally(e)) - ); - try { - RemoteExtensionActionResponse response = futureResponse.orTimeout( - ExtensionsManager.EXTENSION_REQUEST_WAIT_TIMEOUT, - TimeUnit.SECONDS - ).get(); - if (!response.isSuccess()) { - return new ExtensionRestResponse(request, OK, "Remote extension reponse failed: " + response.getResponseBytesAsString()); - } - // Parse out the expected response class from the bytes - SampleResponse sampleResponse = new SampleResponse(StreamInput.wrap(response.getResponseBytes())); - return new ExtensionRestResponse(request, OK, "Received greeting from remote extension: " + sampleResponse.getGreeting()); - } catch (Exception e) { - return exceptionalRequest(request, e); - } - }; - -} diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/schedule/GreetJob.java b/src/main/java/org/opensearch/sdk/sample/helloworld/schedule/GreetJob.java deleted file mode 100644 index be0be3dd..00000000 --- a/src/main/java/org/opensearch/sdk/sample/helloworld/schedule/GreetJob.java +++ /dev/null @@ -1,380 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sdk.sample.helloworld.schedule; - -import com.google.common.base.Objects; -import org.opensearch.common.io.stream.StreamInput; -import org.opensearch.common.io.stream.StreamOutput; -import org.opensearch.common.io.stream.Writeable; -import org.opensearch.core.ParseField; -import org.opensearch.core.xcontent.NamedXContentRegistry; -import org.opensearch.core.xcontent.ToXContent; -import org.opensearch.core.xcontent.ToXContentObject; -import org.opensearch.core.xcontent.XContentBuilder; -import org.opensearch.core.xcontent.XContentParser; -import org.opensearch.jobscheduler.spi.ScheduledJobParameter; -import org.opensearch.jobscheduler.spi.schedule.CronSchedule; -import org.opensearch.jobscheduler.spi.schedule.IntervalSchedule; -import org.opensearch.jobscheduler.spi.schedule.Schedule; -import org.opensearch.jobscheduler.spi.schedule.ScheduleParser; - -import java.io.IOException; -import java.time.Instant; - -import static org.opensearch.common.xcontent.XContentParserUtils.ensureExpectedToken; - -/** - * Sample scheduled job for the HelloWorld Extension - */ -public class GreetJob implements Writeable, ToXContentObject, ScheduledJobParameter { - enum ScheduleType { - CRON, - INTERVAL - } - - public static final String PARSE_FIELD_NAME = "GreetJob"; - public static final NamedXContentRegistry.Entry XCONTENT_REGISTRY = new NamedXContentRegistry.Entry( - GreetJob.class, - new ParseField(PARSE_FIELD_NAME), - it -> parse(it) - ); - - public static final String HELLO_WORLD_JOB_INDEX = ".hello-world-jobs"; - public static final String NAME_FIELD = "name"; - public static final String LAST_UPDATE_TIME_FIELD = "last_update_time"; - public static final String LOCK_DURATION_SECONDS = "lock_duration_seconds"; - - public static final String SCHEDULE_FIELD = "schedule"; - public static final String IS_ENABLED_FIELD = "enabled"; - public static final String ENABLED_TIME_FIELD = "enabled_time"; - public static final String DISABLED_TIME_FIELD = "disabled_time"; - - private final String name; - private final Schedule schedule; - private final Boolean isEnabled; - private final Instant enabledTime; - private final Instant disabledTime; - private final Instant lastUpdateTime; - private final Long lockDurationSeconds; - - /** - * - * @param name name of the scheduled job - * @param schedule The schedule, cron or interval, the job run will run with - * @param isEnabled Flag to indices whether this job is enabled - * @param enabledTime Timestamp when the job was last enabled - * @param disabledTime Timestamp when the job was last disabled - * @param lastUpdateTime Timestamp when the job was last updated - * @param lockDurationSeconds Time in seconds for how long this job should acquire a lock - */ - public GreetJob( - String name, - Schedule schedule, - Boolean isEnabled, - Instant enabledTime, - Instant disabledTime, - Instant lastUpdateTime, - Long lockDurationSeconds - ) { - this.name = name; - this.schedule = schedule; - this.isEnabled = isEnabled; - this.enabledTime = enabledTime; - this.disabledTime = disabledTime; - this.lastUpdateTime = lastUpdateTime; - this.lockDurationSeconds = lockDurationSeconds; - } - - /** - * - * @param input The input stream - * @throws IOException Thrown if there is an error parsing the input stream into a GreetJob - */ - public GreetJob(StreamInput input) throws IOException { - name = input.readString(); - if (input.readEnum(GreetJob.ScheduleType.class) == ScheduleType.CRON) { - schedule = new CronSchedule(input); - } else { - schedule = new IntervalSchedule(input); - } - isEnabled = input.readBoolean(); - enabledTime = input.readInstant(); - disabledTime = input.readInstant(); - lastUpdateTime = input.readInstant(); - lockDurationSeconds = input.readLong(); - } - - /** - * - * @param builder An XContentBuilder instance - * @param params TOXContent.Params - * @return - * @throws IOException - */ - @Override - public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException { - XContentBuilder xContentBuilder = builder.startObject() - .field(NAME_FIELD, name) - .field(SCHEDULE_FIELD, schedule) - .field(IS_ENABLED_FIELD, isEnabled) - .field(ENABLED_TIME_FIELD, enabledTime.toEpochMilli()) - .field(LAST_UPDATE_TIME_FIELD, lastUpdateTime.toEpochMilli()) - .field(LOCK_DURATION_SECONDS, lockDurationSeconds); - if (disabledTime != null) { - xContentBuilder.field(DISABLED_TIME_FIELD, disabledTime.toEpochMilli()); - } - return xContentBuilder.endObject(); - } - - /** - * - * @param output The output stream - * @throws IOException - */ - @Override - public void writeTo(StreamOutput output) throws IOException { - output.writeString(name); - if (schedule instanceof CronSchedule) { - output.writeEnum(ScheduleType.CRON); - } else { - output.writeEnum(ScheduleType.INTERVAL); - } - schedule.writeTo(output); - output.writeBoolean(isEnabled); - output.writeInstant(enabledTime); - output.writeInstant(disabledTime); - output.writeInstant(lastUpdateTime); - output.writeLong(lockDurationSeconds); - } - - /** - * - * @param parser Parser that takes builds a GreetJob from XContent - * @return An instance of a GreetJob - * @throws IOException - */ - public static GreetJob parse(XContentParser parser) throws IOException { - String name = null; - Schedule schedule = null; - // we cannot set it to null as isEnabled() would do the unboxing and results in null pointer exception - Boolean isEnabled = Boolean.FALSE; - Instant enabledTime = null; - Instant disabledTime = null; - Instant lastUpdateTime = null; - Long lockDurationSeconds = 5L; - - ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.currentToken(), parser); - while (parser.nextToken() != XContentParser.Token.END_OBJECT) { - String fieldName = parser.currentName(); - parser.nextToken(); - - switch (fieldName) { - case NAME_FIELD: - name = parser.text(); - break; - case SCHEDULE_FIELD: - schedule = ScheduleParser.parse(parser); - break; - case IS_ENABLED_FIELD: - isEnabled = parser.booleanValue(); - break; - case ENABLED_TIME_FIELD: - enabledTime = toInstant(parser); - break; - case DISABLED_TIME_FIELD: - disabledTime = toInstant(parser); - break; - case LAST_UPDATE_TIME_FIELD: - lastUpdateTime = toInstant(parser); - break; - case LOCK_DURATION_SECONDS: - lockDurationSeconds = parser.longValue(); - break; - default: - parser.skipChildren(); - break; - } - } - return new GreetJob(name, schedule, isEnabled, enabledTime, disabledTime, lastUpdateTime, lockDurationSeconds); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - GreetJob that = (GreetJob) o; - return Objects.equal(getName(), that.getName()) - && Objects.equal(getSchedule(), that.getSchedule()) - && Objects.equal(isEnabled(), that.isEnabled()) - && Objects.equal(getEnabledTime(), that.getEnabledTime()) - && Objects.equal(getDisabledTime(), that.getDisabledTime()) - && Objects.equal(getLastUpdateTime(), that.getLastUpdateTime()) - && Objects.equal(getLockDurationSeconds(), that.getLockDurationSeconds()); - } - - @Override - public int hashCode() { - return Objects.hashCode(name, schedule, isEnabled, enabledTime, lastUpdateTime); - } - - @Override - public String getName() { - return name; - } - - @Override - public Schedule getSchedule() { - return schedule; - } - - @Override - public boolean isEnabled() { - return isEnabled; - } - - @Override - public Instant getEnabledTime() { - return enabledTime; - } - - public Instant getDisabledTime() { - return disabledTime; - } - - @Override - public Instant getLastUpdateTime() { - return lastUpdateTime; - } - - @Override - public Long getLockDurationSeconds() { - return lockDurationSeconds; - } - - /** - * Parse content parser to {@link java.time.Instant}. - * - * @param parser json based content parser - * @return instance of {@link java.time.Instant} - * @throws IOException IOException if content can't be parsed correctly - */ - public static Instant toInstant(XContentParser parser) throws IOException { - if (parser.currentToken() == null || parser.currentToken() == XContentParser.Token.VALUE_NULL) { - return null; - } - if (parser.currentToken().isValue()) { - return Instant.ofEpochMilli(parser.longValue()); - } - return null; - } - - /** - * - * @return Returns a plain old java object of a GreetJob for writing to the hello world jobs index - */ - public GreetJobPojo toPojo() { - GreetJobPojo.SchedulePojo.IntervalPojo interval = null; - if (this.schedule instanceof IntervalSchedule) { - interval = new GreetJobPojo.SchedulePojo.IntervalPojo( - ((IntervalSchedule) this.schedule).getUnit().toString(), - ((IntervalSchedule) this.schedule).getInterval(), - ((IntervalSchedule) this.schedule).getStartTime().toEpochMilli() - ); - } - return new GreetJobPojo( - this.enabledTime.toEpochMilli(), - this.lastUpdateTime.toEpochMilli(), - this.name, - this.lockDurationSeconds.intValue(), - this.isEnabled.booleanValue(), - new GreetJobPojo.SchedulePojo(interval) - ); - } - - /** - * A plain java representation of a GreetJob using only primitives - */ - public static class GreetJobPojo { - public long enabled_time; - public long last_update_time; - - public String name; - - public int lock_duration_seconds; - - public boolean enabled; - - public SchedulePojo schedule; - - /** - * - * @param enabledTime - * @param lastUpdateTime - * @param name - * @param lockDurationSeconds - * @param enabled - * @param schedule - */ - public GreetJobPojo( - long enabledTime, - long lastUpdateTime, - String name, - int lockDurationSeconds, - boolean enabled, - SchedulePojo schedule - ) { - this.enabled_time = enabledTime; - this.last_update_time = lastUpdateTime; - this.name = name; - this.lock_duration_seconds = lockDurationSeconds; - this.enabled = enabled; - this.schedule = schedule; - } - - /** - * A plain java representation of a Schedule using only primitives - */ - public static class SchedulePojo { - - public IntervalPojo interval; - - /** - * - * @param interval An Interval instance - */ - public SchedulePojo(IntervalPojo interval) { - this.interval = interval; - } - - /** - * A plain java representation of a Interval using only primitives - */ - public static class IntervalPojo { - public String unit; - - public int period; - - public long start_time; - - /** - * - * @param unit Unit of time - * @param period Number of units between job execution - * @param start_time The time when the interval first started - */ - public IntervalPojo(String unit, int period, long start_time) { - this.unit = unit; - this.period = period; - this.start_time = start_time; - } - } - } - } -} diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/spec/openapi.json b/src/main/java/org/opensearch/sdk/sample/helloworld/spec/openapi.json deleted file mode 100644 index 85c7da69..00000000 --- a/src/main/java/org/opensearch/sdk/sample/helloworld/spec/openapi.json +++ /dev/null @@ -1,149 +0,0 @@ -{ - "openapi": "3.0.3", - "info": { - "title": "Hello World", - "description": "This is a sample Hello World extension.", - "license": { - "name": "Apache 2.0", - "url": "http://www.apache.org/licenses/LICENSE-2.0.html" - }, - "version": "1.0.0-SNAPSHOT" - }, - "tags": [ - { - "name": "hello", - "description": "Worldly Greetings" - } - ], - "paths": { - "/hello": { - "get": { - "tags": [ - "hello" - ], - "summary": "Greet the world", - "description": "Traditional greeting", - "responses": { - "200": { - "description": "Successful operation", - "content": { - "text/plain; charset=utf-8": { - "examples": { - "Default Response": { - "value": "Hello, World!" - } - } - } - } - }, - "400": { - "description": "Syntax Error in URI" - }, - "404": { - "description": "Improper REST action configuration" - } - } - }, - "post": { - "tags": [ - "hello" - ], - "summary": "Adds a descriptive world adjective to a list", - "description": "Adds an adjective to a list from which a random element will be prepended to the world name", - "operationId": "", - "requestBody": { - "description": "An adjective in plain text or JSON", - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "adjective": { - "type": "string", - "example": "wonderful" - } - } - } - }, - "text/plain": { - "schema": { - "type": "string", - "example": "wonderful" - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation" - }, - "400": { - "description": "Syntax Error in request" - }, - "404": { - "description": "Improper REST action configuration" - }, - "406": { - "description": "Content format not text or JSON" - } - } - } - }, - "/hello/{name}": { - "put": { - "tags": [ - "hello" - ], - "summary": "Rename the world", - "description": "Update the world to a custom name", - "parameters": [ - { - "name": "name", - "in": "path", - "description": "A new name for the world", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Successful operation", - "content": { - "text/plain; charset=utf-8": { - "examples": { - "Default Response": { - "value": "Updated the world's name to OpenSearch" - } - } - } - } - }, - "400": { - "description": "Syntax Error in URI" - }, - "404": { - "description": "Improper REST action configuration" - } - } - } - }, - "/goodbye": { - "delete": { - "tags": [ - "hello" - ], - "summary": "Restores the world to default", - "description": "Removes all adjectives and the custom world name", - "operationId": "", - "responses": { - "200": { - "description": "Successful operation" - } - } - } - } - } -} diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/spec/openapi.yaml b/src/main/java/org/opensearch/sdk/sample/helloworld/spec/openapi.yaml deleted file mode 100644 index b8ffc77b..00000000 --- a/src/main/java/org/opensearch/sdk/sample/helloworld/spec/openapi.yaml +++ /dev/null @@ -1,117 +0,0 @@ -openapi: 3.0.3 -info: - title: Hello World - description: This is a sample Hello World extension. - license: - name: Apache 2.0 - url: http://www.apache.org/licenses/LICENSE-2.0.html - version: 1.0.0-SNAPSHOT -tags: - - name: hello - description: Worldly Greetings -paths: - /hello: - get: - tags: - - hello - summary: Greet the world - description: Traditional greeting - responses: - '200': - description: Successful operation - content: - text/plain; charset=utf-8: - examples: - Default Response: - value: Hello, World! - '400': - description: Syntax Error in URI - '404': - description: Improper REST action configuration - post: - tags: - - hello - summary: Adds a descriptive world adjective to a list - description: >- - Adds an adjective to a list from which a random element will be - prepended to the world name - operationId: '' - requestBody: - description: An adjective in plain text or JSON - required: true - content: - application/json: - schema: - type: object - properties: - adjective: - type: string - example: wonderful - text/plain: - schema: - type: string - example: wonderful - responses: - '200': - description: Successful operation - '400': - description: Syntax Error in request - '404': - description: Improper REST action configuration - '406': - description: Content format not text or JSON - delete: - tags: - - hello - summary: Removes an adjective from the list - description: >- - Removes an adjective from the list from which a random element - will be prepended to the world name - operationId: '' - responses: - '200': - description: Successful operation - '304': - description: Adjective not in the list, no action taken - '400': - description: Syntax Error in request - '404': - description: Improper REST action configuration - '406': - description: Content format not text or JSON - /hello/{name}: - put: - tags: - - hello - summary: Rename the world - description: Update the world to a custom name - parameters: - - name: name - in: path - description: A new name for the world - required: true - schema: - type: string - responses: - '200': - description: Successful operation - content: - text/plain; charset=utf-8: - examples: - Default Response: - value: Updated the world's name to OpenSearch - '400': - description: Syntax Error in URI - '404': - description: Improper REST action configuration - /goodbye: - delete: - tags: - - hello - summary: Restores the world to default - description: >- - Removes all adjectives and the custom world name - operationId: '' - responses: - '200': - description: Successful operation diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobParameterAction.java b/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobParameterAction.java deleted file mode 100644 index 78d09714..00000000 --- a/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobParameterAction.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sdk.sample.helloworld.transport; - -import org.opensearch.action.ActionType; -import org.opensearch.jobscheduler.transport.response.JobParameterResponse; - -/** - * Hello World Job Parameter Action - */ -public class HWJobParameterAction extends ActionType { - public static final String NAME = "extensions:hw/greet_job_parameter"; - public static final HWJobParameterAction INSTANCE = new HWJobParameterAction(); - - private HWJobParameterAction() { - super(NAME, JobParameterResponse::new); - } -} diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobParameterTransportAction.java b/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobParameterTransportAction.java deleted file mode 100644 index e5b05243..00000000 --- a/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobParameterTransportAction.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sdk.sample.helloworld.transport; - -import com.google.inject.Inject; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.opensearch.action.ActionListener; -import org.opensearch.action.support.ActionFilters; -import org.opensearch.action.support.TransportAction; -import org.opensearch.common.xcontent.LoggingDeprecationHandler; -import org.opensearch.common.xcontent.XContentHelper; -import org.opensearch.common.xcontent.XContentType; -import org.opensearch.core.xcontent.XContentParser; -import org.opensearch.jobscheduler.model.ExtensionJobParameter; -import org.opensearch.jobscheduler.spi.ScheduledJobParameter; -import org.opensearch.jobscheduler.transport.request.JobParameterRequest; -import org.opensearch.jobscheduler.transport.response.JobParameterResponse; -import org.opensearch.sdk.SDKNamedXContentRegistry; -import org.opensearch.sdk.sample.helloworld.schedule.GreetJob; -import org.opensearch.tasks.Task; -import org.opensearch.tasks.TaskManager; - -import static org.opensearch.common.xcontent.XContentParserUtils.ensureExpectedToken; -import static org.opensearch.sdk.sample.helloworld.util.RestHandlerUtils.wrapRestActionListener; - -/** - * Hello World Job Parameter Transport Action - */ -public class HWJobParameterTransportAction extends TransportAction { - - private static final Logger LOG = LogManager.getLogger(HWJobParameterTransportAction.class); - - private final SDKNamedXContentRegistry xContentRegistry; - - /** - * Instantiate this action - * - * @param actionFilters Action filters - * @param taskManager The task manager - * @param xContentRegistry The xContentRegistry - */ - @Inject - protected HWJobParameterTransportAction( - ActionFilters actionFilters, - TaskManager taskManager, - SDKNamedXContentRegistry xContentRegistry - ) { - super(HWJobParameterAction.NAME, actionFilters, taskManager); - this.xContentRegistry = xContentRegistry; - } - - @Override - protected void doExecute(Task task, JobParameterRequest request, ActionListener actionListener) { - - String errorMessage = "Failed to parse the Job Parameter"; - ActionListener listener = wrapRestActionListener(actionListener, errorMessage); - try { - XContentParser parser = XContentHelper.createParser( - xContentRegistry.getRegistry(), - LoggingDeprecationHandler.INSTANCE, - request.getJobSource(), - XContentType.JSON - ); - ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser); - ScheduledJobParameter scheduledJobParameter = GreetJob.parse(parser); - JobParameterResponse jobParameterResponse = new JobParameterResponse(new ExtensionJobParameter(scheduledJobParameter)); - listener.onResponse(jobParameterResponse); - } catch (Exception e) { - LOG.error(e); - listener.onFailure(e); - } - } -} diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobRunnerAction.java b/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobRunnerAction.java deleted file mode 100644 index d9c41e97..00000000 --- a/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobRunnerAction.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sdk.sample.helloworld.transport; - -import org.opensearch.action.ActionType; -import org.opensearch.jobscheduler.transport.response.JobRunnerResponse; - -/** - * Hello World Job Runner Action - */ -public class HWJobRunnerAction extends ActionType { - - public static final String NAME = "extensions:hw/greet_job_runner"; - public static final HWJobRunnerAction INSTANCE = new HWJobRunnerAction(); - - private HWJobRunnerAction() { - super(NAME, JobRunnerResponse::new); - } -} diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobRunnerTransportAction.java b/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobRunnerTransportAction.java deleted file mode 100644 index bc3f290c..00000000 --- a/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HWJobRunnerTransportAction.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sdk.sample.helloworld.transport; - -import com.google.inject.Inject; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.opensearch.action.ActionListener; -import org.opensearch.action.get.GetRequest; -import org.opensearch.action.support.ActionFilters; -import org.opensearch.action.support.TransportAction; -import org.opensearch.common.xcontent.LoggingDeprecationHandler; -import org.opensearch.common.xcontent.XContentType; -import org.opensearch.core.xcontent.XContentParser; -import org.opensearch.jobscheduler.spi.JobExecutionContext; -import org.opensearch.jobscheduler.transport.request.JobRunnerRequest; -import org.opensearch.jobscheduler.transport.response.JobRunnerResponse; -import org.opensearch.sdk.SDKClient.SDKRestClient; -import org.opensearch.sdk.SDKNamedXContentRegistry; -import org.opensearch.sdk.sample.helloworld.schedule.GreetJob; -import org.opensearch.tasks.Task; -import org.opensearch.tasks.TaskManager; - -import java.io.IOException; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import static org.opensearch.common.xcontent.XContentParserUtils.ensureExpectedToken; -import static org.opensearch.sdk.sample.helloworld.util.RestHandlerUtils.wrapRestActionListener; - -/** - * Hello World Job Runner Transport Action - */ -public class HWJobRunnerTransportAction extends TransportAction { - - private static final Logger LOG = LogManager.getLogger(HWJobRunnerTransportAction.class); - - private SDKRestClient client; - private final SDKNamedXContentRegistry xContentRegistry; - - /** - * Instantiate this action - * - * @param actionFilters Action filters - * @param taskManager The task manager - * @param xContentRegistry xContentRegistry - * @param client SDKRestClient - */ - @Inject - protected HWJobRunnerTransportAction( - ActionFilters actionFilters, - TaskManager taskManager, - SDKNamedXContentRegistry xContentRegistry, - SDKRestClient client - ) { - super(HWJobRunnerAction.NAME, actionFilters, taskManager); - this.client = client; - this.xContentRegistry = xContentRegistry; - } - - @Override - protected void doExecute(Task task, JobRunnerRequest request, ActionListener actionListener) { - String errorMessage = "Failed to run the Job"; - ActionListener listener = wrapRestActionListener(actionListener, errorMessage); - try { - JobExecutionContext jobExecutionContext = request.getJobExecutionContext(); - String jobParameterDocumentId = jobExecutionContext.getJobId(); - if (jobParameterDocumentId == null || jobParameterDocumentId.isEmpty()) { - listener.onFailure(new IllegalArgumentException("jobParameterDocumentId cannot be empty or null")); - } else { - CompletableFuture inProgressFuture = new CompletableFuture<>(); - findById(jobParameterDocumentId, new ActionListener<>() { - @Override - public void onResponse(GreetJob anomalyDetectorJob) { - inProgressFuture.complete(anomalyDetectorJob); - } - - @Override - public void onFailure(Exception e) { - logger.info("could not find GreetJob with id " + jobParameterDocumentId, e); - inProgressFuture.completeExceptionally(e); - } - }); - - try { - GreetJob scheduledJobParameter = inProgressFuture.orTimeout(10000, TimeUnit.MILLISECONDS).join(); - - JobRunnerResponse jobRunnerResponse; - if (scheduledJobParameter != null && validateJobExecutionContext(jobExecutionContext)) { - jobRunnerResponse = new JobRunnerResponse(true); - } else { - jobRunnerResponse = new JobRunnerResponse(false); - } - listener.onResponse(jobRunnerResponse); - if (jobRunnerResponse.getJobRunnerStatus()) { - HelloWorldJobRunner.getJobRunnerInstance().runJob(scheduledJobParameter, jobExecutionContext); - } - } catch (CompletionException e) { - if (e.getCause() instanceof TimeoutException) { - logger.info(" Request timed out with an exception ", e); - } else { - throw e; - } - } catch (Exception e) { - logger.info(" Could not find Job Parameter due to exception ", e); - } - - } - } catch (Exception e) { - LOG.error(e); - listener.onFailure(e); - } - } - - private void findById(String jobParameterId, ActionListener listener) { - GetRequest getRequest = new GetRequest(GreetJob.HELLO_WORLD_JOB_INDEX, jobParameterId); - try { - client.get(getRequest, ActionListener.wrap(response -> { - if (!response.isExists()) { - listener.onResponse(null); - } else { - try { - XContentParser parser = XContentType.JSON.xContent() - .createParser(xContentRegistry.getRegistry(), LoggingDeprecationHandler.INSTANCE, response.getSourceAsString()); - ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser); - listener.onResponse(GreetJob.parse(parser)); - } catch (IOException e) { - logger.error("IOException occurred finding GreetJob for jobParameterId " + jobParameterId, e); - listener.onFailure(e); - } - } - }, exception -> { - logger.error("Exception occurred finding GreetJob for jobParameterId " + jobParameterId, exception); - listener.onFailure(exception); - })); - } catch (Exception e) { - logger.error("Error occurred finding greet job with jobParameterId " + jobParameterId, e); - listener.onFailure(e); - } - - } - - private boolean validateJobExecutionContext(JobExecutionContext jobExecutionContext) { - if (jobExecutionContext != null - && jobExecutionContext.getJobId() != null - && !jobExecutionContext.getJobId().isEmpty() - && jobExecutionContext.getJobIndexName() != null - && !jobExecutionContext.getJobIndexName().isEmpty() - && jobExecutionContext.getExpectedExecutionTime() != null - && jobExecutionContext.getJobVersion() != null) { - return true; - } - return false; - } -} diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HelloWorldJobRunner.java b/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HelloWorldJobRunner.java deleted file mode 100644 index a46965bd..00000000 --- a/src/main/java/org/opensearch/sdk/sample/helloworld/transport/HelloWorldJobRunner.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sdk.sample.helloworld.transport; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.opensearch.jobscheduler.spi.JobExecutionContext; -import org.opensearch.jobscheduler.spi.ScheduledJobParameter; -import org.opensearch.jobscheduler.spi.ScheduledJobRunner; - -/** - * Hello World Job Runner - */ -public class HelloWorldJobRunner implements ScheduledJobRunner { - private static final Logger log = LogManager.getLogger(HelloWorldJobRunner.class); - - private static HelloWorldJobRunner INSTANCE; - - /** - * - * @return Return or create an instance of this job runner - */ - public static HelloWorldJobRunner getJobRunnerInstance() { - if (INSTANCE != null) { - return INSTANCE; - } - synchronized (HelloWorldJobRunner.class) { - if (INSTANCE != null) { - return INSTANCE; - } - INSTANCE = new HelloWorldJobRunner(); - return INSTANCE; - } - } - - @Override - public void runJob(ScheduledJobParameter job, JobExecutionContext context) { - System.out.println("Hello, world!"); - } -} diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/transport/SampleAction.java b/src/main/java/org/opensearch/sdk/sample/helloworld/transport/SampleAction.java deleted file mode 100644 index 59918f76..00000000 --- a/src/main/java/org/opensearch/sdk/sample/helloworld/transport/SampleAction.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sdk.sample.helloworld.transport; - -import org.opensearch.action.ActionType; - -/** - * A sample {@link ActionType} used as they key for the action map - */ -public class SampleAction extends ActionType { - - /** - * The name to look up this action with - */ - public static final String NAME = "helloworld/sample"; - /** - * The singleton instance of this class - */ - public static final SampleAction INSTANCE = new SampleAction(); - - private SampleAction() { - super(NAME, SampleResponse::new); - } -} diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/transport/SampleRequest.java b/src/main/java/org/opensearch/sdk/sample/helloworld/transport/SampleRequest.java deleted file mode 100644 index 38f1eade..00000000 --- a/src/main/java/org/opensearch/sdk/sample/helloworld/transport/SampleRequest.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sdk.sample.helloworld.transport; - -import java.io.IOException; - -import org.opensearch.action.ActionRequest; -import org.opensearch.action.ActionRequestValidationException; -import org.opensearch.common.io.stream.StreamInput; -import org.opensearch.common.io.stream.StreamOutput; - -/** - * A sample request class to demonstrate extension actions - */ -public class SampleRequest extends ActionRequest { - - private final String name; - - /** - * Instantiate this request - * - * @param name A name to pass to the action - */ - public SampleRequest(String name) { - this.name = name; - } - - /** - * Instantiate this request from a byte stream - * - * @param in the byte stream - * @throws IOException on failure reading the stream - */ - public SampleRequest(StreamInput in) throws IOException { - this.name = in.readString(); - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - out.writeString(name); - } - - @Override - public ActionRequestValidationException validate() { - return null; - } - - public String getName() { - return name; - } -} diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/transport/SampleResponse.java b/src/main/java/org/opensearch/sdk/sample/helloworld/transport/SampleResponse.java deleted file mode 100644 index 80e60706..00000000 --- a/src/main/java/org/opensearch/sdk/sample/helloworld/transport/SampleResponse.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sdk.sample.helloworld.transport; - -import java.io.IOException; - -import org.opensearch.action.ActionResponse; -import org.opensearch.common.io.stream.StreamInput; -import org.opensearch.common.io.stream.StreamOutput; - -/** - * A sample response class to demonstrate extension actions - */ -public class SampleResponse extends ActionResponse { - - private final String greeting; - - /** - * Instantiate this response - * - * @param greeting The greeting to return - */ - public SampleResponse(String greeting) { - this.greeting = greeting; - } - - /** - * Instantiate this response from a byte stream - * - * @param in the byte stream - * @throws IOException on failure reading the stream - */ - public SampleResponse(StreamInput in) throws IOException { - this.greeting = in.readString(); - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - out.writeString(greeting); - } - - public String getGreeting() { - return greeting; - } -} diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/transport/SampleTransportAction.java b/src/main/java/org/opensearch/sdk/sample/helloworld/transport/SampleTransportAction.java deleted file mode 100644 index 2c80f1cf..00000000 --- a/src/main/java/org/opensearch/sdk/sample/helloworld/transport/SampleTransportAction.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sdk.sample.helloworld.transport; - -import org.opensearch.action.ActionListener; -import org.opensearch.action.support.ActionFilters; -import org.opensearch.action.support.TransportAction; -import org.opensearch.tasks.Task; -import org.opensearch.tasks.TaskManager; - -import com.google.inject.Inject; - -/** - * A sample {@link TransportAction} used as they value for the action map - */ -public class SampleTransportAction extends TransportAction { - - /** - * Instantiate this action - * - * @param actionName The action name - * @param actionFilters Action filters - * @param taskManager The task manager - */ - @Inject - protected SampleTransportAction(String actionName, ActionFilters actionFilters, TaskManager taskManager) { - super(actionName, actionFilters, taskManager); - } - - @Override - protected void doExecute(Task task, SampleRequest request, ActionListener listener) { - // Fail if name is empty - if (request.getName().isBlank()) { - listener.onFailure(new IllegalArgumentException("The request name is blank.")); - } else { - listener.onResponse(new SampleResponse("Hello, " + request.getName())); - } - } -} diff --git a/src/main/java/org/opensearch/sdk/sample/helloworld/util/RestHandlerUtils.java b/src/main/java/org/opensearch/sdk/sample/helloworld/util/RestHandlerUtils.java deleted file mode 100644 index 84172973..00000000 --- a/src/main/java/org/opensearch/sdk/sample/helloworld/util/RestHandlerUtils.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sdk.sample.helloworld.util; - -import com.google.common.base.Throwables; -import com.google.common.collect.ImmutableMap; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.opensearch.OpenSearchStatusException; -import org.opensearch.action.ActionListener; -import org.opensearch.core.xcontent.ToXContent; -import org.opensearch.index.IndexNotFoundException; -import org.opensearch.indices.InvalidIndexNameException; -import org.opensearch.rest.RestStatus; - -import static org.opensearch.rest.RestStatus.BAD_REQUEST; -import static org.opensearch.rest.RestStatus.INTERNAL_SERVER_ERROR; - -/** - * Utility functions for REST handlers. - */ -public final class RestHandlerUtils { - private static final Logger logger = LogManager.getLogger(RestHandlerUtils.class); - - public static final ToXContent.MapParams XCONTENT_WITH_TYPE = new ToXContent.MapParams(ImmutableMap.of("with_type", "true")); - - private RestHandlerUtils() {} - - /** - * Wrap action listener to avoid return verbose error message and wrong 500 error to user. - * Suggestion for exception handling in HW: - * 1. If the error is caused by wrong input, throw IllegalArgumentException exception. - * 2. For other errors, please use OpenSearchStatusException. - * - * TODO: tune this function for wrapped exception, return root exception error message - * - * @param actionListener action listener - * @param generalErrorMessage general error message - * @param action listener response type - * @return wrapped action listener - */ - public static ActionListener wrapRestActionListener(ActionListener actionListener, String generalErrorMessage) { - return ActionListener.wrap(r -> { actionListener.onResponse(r); }, e -> { - logger.error("Wrap exception before sending back to user", e); - Throwable cause = Throwables.getRootCause(e); - if (isProperExceptionToReturn(e)) { - actionListener.onFailure(e); - } else if (isProperExceptionToReturn(cause)) { - actionListener.onFailure((Exception) cause); - } else { - RestStatus status = isBadRequest(e) ? BAD_REQUEST : INTERNAL_SERVER_ERROR; - String errorMessage = generalErrorMessage; - if (isBadRequest(e)) { - errorMessage = e.getMessage(); - } else if (cause != null && isBadRequest(cause)) { - errorMessage = cause.getMessage(); - } - actionListener.onFailure(new OpenSearchStatusException(errorMessage, status)); - } - }); - } - - /** - * @param e Throwable - * @return Return a boolean whether there was an exception thrown on a request - */ - public static boolean isBadRequest(Throwable e) { - if (e == null) { - return false; - } - return e instanceof IllegalArgumentException; - } - - /** - * @param e Throwable - * @return Return boolean if throwable is of a proper exception type - */ - public static boolean isProperExceptionToReturn(Throwable e) { - if (e == null) { - return false; - } - return e instanceof OpenSearchStatusException || e instanceof IndexNotFoundException || e instanceof InvalidIndexNameException; - } -} From 51a1ddaa4e7aa9db3e7076d2c8a7e476225055e0 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Mon, 1 May 2023 17:49:47 -0400 Subject: [PATCH 08/16] Fix TestHelloWorldExtension Signed-off-by: Craig Perkins --- sample/build.gradle | 2 ++ .../sample/helloworld/TestHelloWorldExtension.java | 14 +++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/sample/build.gradle b/sample/build.gradle index 44188a73..e9cb4fa0 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -68,6 +68,8 @@ publishing { } test { + getTestFrameworkProperty().convention(getProviderFactory().provider(() -> new JUnitPlatformTestFramework(it.getFilter(), false))) + jvmArgs '--enable-preview' systemProperty 'tests.security.manager', 'false' } diff --git a/sample/src/test/java/org/opensearch/sdk/sample/helloworld/TestHelloWorldExtension.java b/sample/src/test/java/org/opensearch/sdk/sample/helloworld/TestHelloWorldExtension.java index 1d114869..ce6297ef 100644 --- a/sample/src/test/java/org/opensearch/sdk/sample/helloworld/TestHelloWorldExtension.java +++ b/sample/src/test/java/org/opensearch/sdk/sample/helloworld/TestHelloWorldExtension.java @@ -25,8 +25,10 @@ import org.opensearch.action.ActionType; import org.opensearch.action.support.TransportAction; import org.opensearch.common.settings.Settings; +import org.opensearch.sdk.SDKNamedXContentRegistry; import org.opensearch.sdk.api.ActionExtension.ActionHandler; import org.opensearch.sdk.rest.ExtensionRestHandler; +import org.opensearch.sdk.rest.SDKRestRequest; import org.opensearch.sdk.sample.helloworld.transport.SampleAction; import org.opensearch.sdk.sample.helloworld.transport.SampleRequest; import org.opensearch.sdk.sample.helloworld.transport.SampleResponse; @@ -73,11 +75,17 @@ public void setUp() throws Exception { Settings settings = Settings.builder().put(ExtensionsRunner.NODE_NAME_SETTING, "test").build(); ThreadPool threadPool = new ThreadPool(settings); TaskManager taskManager = new TaskManager(settings, threadPool, Collections.emptySet()); + SDKNamedXContentRegistry xContentRegistry = SDKNamedXContentRegistry.EMPTY; this.sdkClient = new SDKClient(extensionSettings); - this.injector = Guice.createInjector(new SDKActionModule(extension), b -> { + sdkClient.initialize(Map.of()); + SDKRestClient restClient = sdkClient.initializeRestClient("localhost", 9200); + SDKActionModule actionModule = new SDKActionModule(extension); + this.injector = Guice.createInjector(actionModule, b -> { b.bind(ThreadPool.class).toInstance(threadPool); b.bind(TaskManager.class).toInstance(taskManager); b.bind(SDKClient.class).toInstance(sdkClient); + b.bind(SDKNamedXContentRegistry.class).toInstance(xContentRegistry); + b.bind(SDKClient.SDKRestClient.class).toInstance(restClient); }); initializeSdkClient(); this.sdkRestClient = sdkClient.initializeRestClient("localhost", 9200); @@ -112,13 +120,13 @@ public void testExtensionRestHandlers() { List extensionRestHandlers = extension.getExtensionRestHandlers(); assertEquals(2, extensionRestHandlers.size()); assertEquals(4, extensionRestHandlers.get(0).routes().size()); - assertEquals(1, extensionRestHandlers.get(1).routes().size()); + assertEquals(2, extensionRestHandlers.get(1).routes().size()); } @Test public void testGetActions() { List> actions = extension.getActions(); - assertEquals(1, actions.size()); + assertEquals(3, actions.size()); } @Test From 90fb2e79f988881f28a121c6518435fca63a3974 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Mon, 1 May 2023 17:53:26 -0400 Subject: [PATCH 09/16] Run spotlessApply Signed-off-by: Craig Perkins --- .../opensearch/sdk/sample/helloworld/HelloWorldExtension.java | 1 - .../opensearch/sdk/sample/helloworld/transport/SampleAction.java | 1 - .../sdk/sample/helloworld/TestHelloWorldExtension.java | 1 - 3 files changed, 3 deletions(-) diff --git a/sample/src/main/java/org/opensearch/sdk/sample/helloworld/HelloWorldExtension.java b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/HelloWorldExtension.java index 598b12a0..c4fc8ba3 100644 --- a/sample/src/main/java/org/opensearch/sdk/sample/helloworld/HelloWorldExtension.java +++ b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/HelloWorldExtension.java @@ -27,7 +27,6 @@ import org.opensearch.sdk.SDKClient; import org.opensearch.sdk.api.ActionExtension; import org.opensearch.sdk.rest.ExtensionRestHandler; -import org.opensearch.sdk.sample.helloworld.ExampleCustomSettingConfig; import org.opensearch.sdk.sample.helloworld.rest.RestHelloAction; import org.opensearch.sdk.sample.helloworld.rest.RestRemoteHelloAction; import org.opensearch.sdk.sample.helloworld.schedule.GreetJob; diff --git a/sample/src/main/java/org/opensearch/sdk/sample/helloworld/transport/SampleAction.java b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/transport/SampleAction.java index 0c125b73..59918f76 100644 --- a/sample/src/main/java/org/opensearch/sdk/sample/helloworld/transport/SampleAction.java +++ b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/transport/SampleAction.java @@ -10,7 +10,6 @@ package org.opensearch.sdk.sample.helloworld.transport; import org.opensearch.action.ActionType; -import org.opensearch.sdk.sample.helloworld.transport.SampleResponse; /** * A sample {@link ActionType} used as they key for the action map diff --git a/sample/src/test/java/org/opensearch/sdk/sample/helloworld/TestHelloWorldExtension.java b/sample/src/test/java/org/opensearch/sdk/sample/helloworld/TestHelloWorldExtension.java index ce6297ef..70e94c65 100644 --- a/sample/src/test/java/org/opensearch/sdk/sample/helloworld/TestHelloWorldExtension.java +++ b/sample/src/test/java/org/opensearch/sdk/sample/helloworld/TestHelloWorldExtension.java @@ -28,7 +28,6 @@ import org.opensearch.sdk.SDKNamedXContentRegistry; import org.opensearch.sdk.api.ActionExtension.ActionHandler; import org.opensearch.sdk.rest.ExtensionRestHandler; -import org.opensearch.sdk.rest.SDKRestRequest; import org.opensearch.sdk.sample.helloworld.transport.SampleAction; import org.opensearch.sdk.sample.helloworld.transport.SampleRequest; import org.opensearch.sdk.sample.helloworld.transport.SampleResponse; From 7c03144dc025bab361517664dd813452a5e934ce Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Tue, 2 May 2023 09:58:40 -0400 Subject: [PATCH 10/16] Temporarily add org.opensearch.plugin Signed-off-by: Craig Perkins --- sample/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/sample/build.gradle b/sample/build.gradle index e9cb4fa0..4b5a7713 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -81,6 +81,7 @@ dependencies { implementation project(':') implementation("org.opensearch.plugin:opensearch-job-scheduler:${jobSchedulerVersion}") + implementation("org.opensearch.plugin:opensearch-job-scheduler-spi:${jobSchedulerVersion}") implementation("org.opensearch:opensearch-job-scheduler-spi:${jobSchedulerVersion}") testImplementation project(':').sourceSets.test.output From 69862c3038f4759b61c0240f260fec6450770ff5 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Tue, 2 May 2023 10:04:04 -0400 Subject: [PATCH 11/16] Update build.gradle Signed-off-by: Craig Perkins --- sample/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sample/build.gradle b/sample/build.gradle index 4b5a7713..6a09bdd9 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -81,7 +81,7 @@ dependencies { implementation project(':') implementation("org.opensearch.plugin:opensearch-job-scheduler:${jobSchedulerVersion}") - implementation("org.opensearch.plugin:opensearch-job-scheduler-spi:${jobSchedulerVersion}") + implementation("org.opensearch:opensearch-job-scheduler:${jobSchedulerVersion}") implementation("org.opensearch:opensearch-job-scheduler-spi:${jobSchedulerVersion}") testImplementation project(':').sourceSets.test.output From 7b5378d5e8267a96c9644a3bd24590fe7d349f8d Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Tue, 9 May 2023 10:42:15 -0400 Subject: [PATCH 12/16] Remove extra line in sample/build.gradle Signed-off-by: Craig Perkins --- sample/build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/sample/build.gradle b/sample/build.gradle index 6a09bdd9..4a436860 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -80,7 +80,6 @@ dependencies { def junitPlatform = "1.9.3" implementation project(':') - implementation("org.opensearch.plugin:opensearch-job-scheduler:${jobSchedulerVersion}") implementation("org.opensearch:opensearch-job-scheduler:${jobSchedulerVersion}") implementation("org.opensearch:opensearch-job-scheduler-spi:${jobSchedulerVersion}") From 0f42ca48d549354db5a990572ff51edb812dbe99 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Wed, 7 Jun 2023 09:50:45 -0400 Subject: [PATCH 13/16] Run spotlessApply Signed-off-by: Craig Perkins --- sample/build.gradle | 2 +- .../sdk/sample/helloworld/TestHelloWorldExtension.java | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/sample/build.gradle b/sample/build.gradle index 4a436860..b11c6435 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -13,7 +13,7 @@ import org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestFram plugins { id 'java' - id "com.diffplug.spotless" version "6.18.0" apply false + id "com.diffplug.spotless" version "6.19.0" apply false id 'jacoco' id "com.form.diff-coverage" version "0.9.5" // for javadocs and checks spotless doesn't do diff --git a/sample/src/test/java/org/opensearch/sdk/sample/helloworld/TestHelloWorldExtension.java b/sample/src/test/java/org/opensearch/sdk/sample/helloworld/TestHelloWorldExtension.java index 7c4b83d5..4b85d044 100644 --- a/sample/src/test/java/org/opensearch/sdk/sample/helloworld/TestHelloWorldExtension.java +++ b/sample/src/test/java/org/opensearch/sdk/sample/helloworld/TestHelloWorldExtension.java @@ -30,7 +30,6 @@ import org.opensearch.client.transport.rest_client.RestClientTransport; import org.opensearch.common.settings.Settings; import org.opensearch.sdk.SDKNamedXContentRegistry; -import org.opensearch.common.transport.TransportAddress; import org.opensearch.sdk.api.ActionExtension.ActionHandler; import org.opensearch.sdk.rest.ExtensionRestHandler; import org.opensearch.sdk.sample.helloworld.transport.SampleAction; From 36bb9451b26271c980177d3abc35c1dfec0ea2d7 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Wed, 7 Jun 2023 10:05:26 -0400 Subject: [PATCH 14/16] Make guava api dependency Signed-off-by: Craig Perkins --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c4a646b4..8bccaa12 100644 --- a/build.gradle +++ b/build.gradle @@ -168,7 +168,7 @@ dependencies { implementation("com.fasterxml.jackson.datatype:jackson-datatype-guava:${jacksonDatabindVersion}") implementation("com.fasterxml.jackson.core:jackson-annotations:${jacksonDatabindVersion}") - implementation("com.google.guava:guava:${guavaVersion}") + api("com.google.guava:guava:${guavaVersion}") implementation("javax.inject:javax.inject:${javaxVersion}") implementation("com.google.guava:failureaccess:${guavaFailureAccessVersion}") implementation("aopalliance:aopalliance:${aopallianceVersion}") From 20577ac51b0e2535ed6814b9808b94395f586d45 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Mon, 10 Jul 2023 13:09:22 -0400 Subject: [PATCH 15/16] Update PR to incorporate NamedRoute change Signed-off-by: Craig Perkins --- .../rest/RestRemoteHelloAction.java | 27 ++++++++++++++----- .../resources/sample/helloworld-settings.yml | 2 +- .../resources/sample/helloworld-settings.yml | 11 -------- 3 files changed, 21 insertions(+), 19 deletions(-) delete mode 100644 src/main/resources/sample/helloworld-settings.yml diff --git a/sample/src/main/java/org/opensearch/sdk/sample/helloworld/rest/RestRemoteHelloAction.java b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/rest/RestRemoteHelloAction.java index be6e1220..6a49ea0f 100644 --- a/sample/src/main/java/org/opensearch/sdk/sample/helloworld/rest/RestRemoteHelloAction.java +++ b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/rest/RestRemoteHelloAction.java @@ -38,7 +38,9 @@ import org.opensearch.jobscheduler.rest.request.GetJobDetailsRequest; import org.opensearch.jobscheduler.spi.schedule.IntervalSchedule; import org.opensearch.jobscheduler.spi.schedule.Schedule; +import org.opensearch.rest.NamedRoute; import org.opensearch.rest.RestRequest; +import org.opensearch.rest.RestResponse; import org.opensearch.rest.RestStatus; import org.opensearch.sdk.ExtensionsRunner; import org.opensearch.sdk.SDKClient; @@ -58,6 +60,7 @@ import java.time.Duration; import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Random; @@ -85,10 +88,20 @@ public RestRemoteHelloAction(ExtensionsRunner runner) { } @Override - public List routeHandlers() { + public List routes() { return List.of( - new RouteHandler(GET, "/hello/{name}", handleRemoteGetRequest), - new RouteHandler(PUT, "/schedule/hello", handleScheduleRequest) + new NamedRoute.Builder().method(GET) + .path("/hello/{name}") + .handler(handleRemoteGetRequest) + .uniqueName(routePrefix("remote_greet_with_name")) + .legacyActionNames(Collections.emptySet()) + .build(), + new NamedRoute.Builder().method(GET) + .path("/schedule/hello") + .handler(handleScheduleRequest) + .uniqueName(routePrefix("scheduled_greet")) + .legacyActionNames(Collections.emptySet()) + .build() ); } @@ -100,7 +113,7 @@ private void registerJobDetails(SDKClient.SDKRestClient client) throws IOExcepti requestBody.field(GetJobDetailsRequest.JOB_TYPE, GreetJob.PARSE_FIELD_NAME); requestBody.field(GetJobDetailsRequest.JOB_PARAMETER_ACTION, HWJobParameterAction.class.getName()); requestBody.field(GetJobDetailsRequest.JOB_RUNNER_ACTION, HWJobRunnerAction.class.getName()); - requestBody.field(GetJobDetailsRequest.EXTENSION_UNIQUE_ID, extensionsRunner.getUniqueId()); + requestBody.field(GetJobDetailsRequest.EXTENSION_UNIQUE_ID, extensionsRunner.getSdkTransportService().getUniqueId()); requestBody.endObject(); Request request = new Request("PUT", String.format(Locale.ROOT, "%s/%s", JobSchedulerPlugin.JS_BASE_URI, "_job_details")); @@ -156,7 +169,7 @@ public T fromJson(String json, Class clazz) { return mapper.deserialize(parser, clazz); } - private Function handleScheduleRequest = (request) -> { + private Function handleScheduleRequest = (request) -> { SDKClient client = extensionsRunner.getSdkClient(); SDKClient.SDKRestClient restClient = client.initializeRestClient(); OpenSearchClient javaClient = client.initializeJavaClient(); @@ -223,7 +236,7 @@ public T fromJson(String json, Class clazz) { return new ExtensionRestResponse(request, OK, "GreetJob successfully scheduled"); }; - private Function handleRemoteGetRequest = (request) -> { + private Function handleRemoteGetRequest = (request) -> { SDKClient client = extensionsRunner.getSdkClient(); String name = request.param("name"); @@ -250,7 +263,7 @@ public T fromJson(String json, Class clazz) { TimeUnit.SECONDS ).get(); if (!response.isSuccess()) { - return new ExtensionRestResponse(request, OK, "Remote extension reponse failed: " + response.getResponseBytesAsString()); + return new ExtensionRestResponse(request, OK, "Remote extension response failed: " + response.getResponseBytesAsString()); } // Parse out the expected response class from the bytes SampleResponse sampleResponse = new SampleResponse(StreamInput.wrap(response.getResponseBytes())); diff --git a/sample/src/main/resources/sample/helloworld-settings.yml b/sample/src/main/resources/sample/helloworld-settings.yml index 417136e2..50881050 100644 --- a/sample/src/main/resources/sample/helloworld-settings.yml +++ b/sample/src/main/resources/sample/helloworld-settings.yml @@ -1,4 +1,4 @@ -extensionName: hello-world +extensionName: hw hostAddress: 127.0.0.1 hostPort: 4532 opensearchAddress: 127.0.0.1 diff --git a/src/main/resources/sample/helloworld-settings.yml b/src/main/resources/sample/helloworld-settings.yml deleted file mode 100644 index 15cdbdcf..00000000 --- a/src/main/resources/sample/helloworld-settings.yml +++ /dev/null @@ -1,11 +0,0 @@ -extensionName: hello-world -hostAddress: 127.0.0.1 -hostPort: 4500 -opensearchAddress: 127.0.0.1 -opensearchPort: 9200 -#ssl.transport.enabled: true -#ssl.transport.pemcert_filepath: certs/extension-01.pem -#ssl.transport.pemkey_filepath: certs/extension-01-key.pem -#ssl.transport.pemtrustedcas_filepath: certs/root-ca.pem -#ssl.transport.enforce_hostname_verification: false -#path.home: From 9f805c2bccf7a91b18b39fcf545e4ea74fab4ecc Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Mon, 10 Jul 2023 13:45:34 -0400 Subject: [PATCH 16/16] Run spotlessApply Signed-off-by: Craig Perkins --- .../rest/RestRemoteHelloAction.java | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/sample/src/main/java/org/opensearch/sdk/sample/helloworld/rest/RestRemoteHelloAction.java b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/rest/RestRemoteHelloAction.java index 6a49ea0f..bb444839 100644 --- a/sample/src/main/java/org/opensearch/sdk/sample/helloworld/rest/RestRemoteHelloAction.java +++ b/sample/src/main/java/org/opensearch/sdk/sample/helloworld/rest/RestRemoteHelloAction.java @@ -69,7 +69,6 @@ import java.util.function.Function; import static org.opensearch.rest.RestRequest.Method.GET; -import static org.opensearch.rest.RestRequest.Method.PUT; import static org.opensearch.rest.RestStatus.OK; /** @@ -90,18 +89,18 @@ public RestRemoteHelloAction(ExtensionsRunner runner) { @Override public List routes() { return List.of( - new NamedRoute.Builder().method(GET) - .path("/hello/{name}") - .handler(handleRemoteGetRequest) - .uniqueName(routePrefix("remote_greet_with_name")) - .legacyActionNames(Collections.emptySet()) - .build(), - new NamedRoute.Builder().method(GET) - .path("/schedule/hello") - .handler(handleScheduleRequest) - .uniqueName(routePrefix("scheduled_greet")) - .legacyActionNames(Collections.emptySet()) - .build() + new NamedRoute.Builder().method(GET) + .path("/hello/{name}") + .handler(handleRemoteGetRequest) + .uniqueName(routePrefix("remote_greet_with_name")) + .legacyActionNames(Collections.emptySet()) + .build(), + new NamedRoute.Builder().method(GET) + .path("/schedule/hello") + .handler(handleScheduleRequest) + .uniqueName(routePrefix("scheduled_greet")) + .legacyActionNames(Collections.emptySet()) + .build() ); }