From a31bc23510a77a6b45bb605d01f4297bc64b8332 Mon Sep 17 00:00:00 2001 From: Shinsuke Sugaya Date: Sat, 21 Mar 2026 17:23:59 +0900 Subject: [PATCH 1/2] feat(action): implement View, Ingestion, Scale and RemoteStore APIs for OpenSearch 3 Add HTTP action implementations for OpenSearch 3 APIs that were previously throwing UnsupportedOperationException: - View API: create, get, delete, update, search, and list view names - Streaming Ingestion API: pause, resume, and get ingestion state - Scale Index API: search-only scaling support - Remote Store Metadata API: cluster remote store metadata retrieval - CreateIndex: add support for context parameter in OpenSearch 3 Includes comprehensive unit tests for all new actions and ByteArrayStreamOutput. --- .../fesen/client/HttpAbstractClient.java | 42 +++--- .../org/codelibs/fesen/client/HttpClient.java | 91 ++++++++++++- .../fesen/client/HttpIndicesAdminClient.java | 30 ++-- .../client/action/HttpCreateIndexAction.java | 25 ++-- .../client/action/HttpCreateViewAction.java | 73 ++++++++++ .../client/action/HttpDeleteViewAction.java | 50 +++++++ .../action/HttpGetIngestionStateAction.java | 119 ++++++++++++++++ .../client/action/HttpGetViewAction.java | 49 +++++++ .../action/HttpListViewNamesAction.java | 60 ++++++++ .../action/HttpPauseIngestionAction.java | 99 ++++++++++++++ .../action/HttpRemoteStoreMetadataAction.java | 123 +++++++++++++++++ .../action/HttpResumeIngestionAction.java | 128 ++++++++++++++++++ .../client/action/HttpScaleIndexAction.java | 60 ++++++++ .../client/action/HttpSearchViewAction.java | 102 ++++++++++++++ .../client/action/HttpUpdateViewAction.java | 74 ++++++++++ .../io/stream/ByteArrayStreamOutput.java | 2 +- .../action/HttpCreateIndexActionTest.java | 113 ++++++++++++++++ .../action/HttpCreateViewActionTest.java | 100 ++++++++++++++ .../action/HttpDeleteViewActionTest.java | 36 +++++ .../HttpGetIngestionStateActionTest.java | 104 ++++++++++++++ .../client/action/HttpGetViewActionTest.java | 36 +++++ .../action/HttpListViewNamesActionTest.java | 121 +++++++++++++++++ .../action/HttpPauseIngestionActionTest.java | 91 +++++++++++++ .../HttpRemoteStoreMetadataActionTest.java | 104 ++++++++++++++ .../action/HttpResumeIngestionActionTest.java | 90 ++++++++++++ .../action/HttpScaleIndexActionTest.java | 30 ++++ .../action/HttpSearchViewActionTest.java | 85 ++++++++++++ .../action/HttpUpdateViewActionTest.java | 97 +++++++++++++ .../io/stream/ByteArrayStreamOutputTest.java | 116 ++++++++++++++++ 29 files changed, 2199 insertions(+), 51 deletions(-) create mode 100644 src/main/java/org/codelibs/fesen/client/action/HttpCreateViewAction.java create mode 100644 src/main/java/org/codelibs/fesen/client/action/HttpDeleteViewAction.java create mode 100644 src/main/java/org/codelibs/fesen/client/action/HttpGetIngestionStateAction.java create mode 100644 src/main/java/org/codelibs/fesen/client/action/HttpGetViewAction.java create mode 100644 src/main/java/org/codelibs/fesen/client/action/HttpListViewNamesAction.java create mode 100644 src/main/java/org/codelibs/fesen/client/action/HttpPauseIngestionAction.java create mode 100644 src/main/java/org/codelibs/fesen/client/action/HttpRemoteStoreMetadataAction.java create mode 100644 src/main/java/org/codelibs/fesen/client/action/HttpResumeIngestionAction.java create mode 100644 src/main/java/org/codelibs/fesen/client/action/HttpScaleIndexAction.java create mode 100644 src/main/java/org/codelibs/fesen/client/action/HttpSearchViewAction.java create mode 100644 src/main/java/org/codelibs/fesen/client/action/HttpUpdateViewAction.java create mode 100644 src/test/java/org/codelibs/fesen/client/action/HttpCreateIndexActionTest.java create mode 100644 src/test/java/org/codelibs/fesen/client/action/HttpCreateViewActionTest.java create mode 100644 src/test/java/org/codelibs/fesen/client/action/HttpDeleteViewActionTest.java create mode 100644 src/test/java/org/codelibs/fesen/client/action/HttpGetIngestionStateActionTest.java create mode 100644 src/test/java/org/codelibs/fesen/client/action/HttpGetViewActionTest.java create mode 100644 src/test/java/org/codelibs/fesen/client/action/HttpListViewNamesActionTest.java create mode 100644 src/test/java/org/codelibs/fesen/client/action/HttpPauseIngestionActionTest.java create mode 100644 src/test/java/org/codelibs/fesen/client/action/HttpRemoteStoreMetadataActionTest.java create mode 100644 src/test/java/org/codelibs/fesen/client/action/HttpResumeIngestionActionTest.java create mode 100644 src/test/java/org/codelibs/fesen/client/action/HttpScaleIndexActionTest.java create mode 100644 src/test/java/org/codelibs/fesen/client/action/HttpSearchViewActionTest.java create mode 100644 src/test/java/org/codelibs/fesen/client/action/HttpUpdateViewActionTest.java create mode 100644 src/test/java/org/codelibs/fesen/client/io/stream/ByteArrayStreamOutputTest.java diff --git a/src/main/java/org/codelibs/fesen/client/HttpAbstractClient.java b/src/main/java/org/codelibs/fesen/client/HttpAbstractClient.java index 3b7309c..80faa30 100644 --- a/src/main/java/org/codelibs/fesen/client/HttpAbstractClient.java +++ b/src/main/java/org/codelibs/fesen/client/HttpAbstractClient.java @@ -102,6 +102,7 @@ import org.opensearch.action.admin.cluster.node.usage.NodesUsageRequest; import org.opensearch.action.admin.cluster.node.usage.NodesUsageRequestBuilder; import org.opensearch.action.admin.cluster.node.usage.NodesUsageResponse; +import org.opensearch.action.admin.cluster.remotestore.metadata.RemoteStoreMetadataAction; import org.opensearch.action.admin.cluster.remotestore.metadata.RemoteStoreMetadataRequestBuilder; import org.opensearch.action.admin.cluster.remotestore.restore.RestoreRemoteStoreAction; import org.opensearch.action.admin.cluster.remotestore.restore.RestoreRemoteStoreRequest; @@ -286,6 +287,9 @@ import org.opensearch.action.admin.indices.rollover.RolloverRequestBuilder; import org.opensearch.action.admin.indices.rollover.RolloverResponse; import org.opensearch.action.admin.indices.scale.searchonly.ScaleIndexRequestBuilder; +import org.opensearch.action.admin.indices.streamingingestion.pause.PauseIngestionAction; +import org.opensearch.action.admin.indices.streamingingestion.resume.ResumeIngestionAction; +import org.opensearch.action.admin.indices.streamingingestion.state.GetIngestionStateAction; import org.opensearch.action.admin.indices.segments.IndicesSegmentResponse; import org.opensearch.action.admin.indices.segments.IndicesSegmentsAction; import org.opensearch.action.admin.indices.segments.IndicesSegmentsRequest; @@ -339,6 +343,10 @@ import org.opensearch.action.admin.indices.validate.query.ValidateQueryRequest; import org.opensearch.action.admin.indices.validate.query.ValidateQueryRequestBuilder; import org.opensearch.action.admin.indices.validate.query.ValidateQueryResponse; +import org.opensearch.action.admin.indices.view.CreateViewAction; +import org.opensearch.action.admin.indices.view.DeleteViewAction; +import org.opensearch.action.admin.indices.view.GetViewAction; +import org.opensearch.action.admin.indices.view.UpdateViewAction; import org.opensearch.action.bulk.BulkAction; import org.opensearch.action.bulk.BulkRequest; import org.opensearch.action.bulk.BulkRequestBuilder; @@ -1521,13 +1529,13 @@ public void wlmStats(WlmStatsRequest request, ActionListener l @Override public RemoteStoreMetadataRequestBuilder prepareRemoteStoreMetadata(final String clusterUUID, final String clusterName) { - throw new UnsupportedOperationException("prepareRemoteStoreMetadata is not supported"); + return new RemoteStoreMetadataRequestBuilder(this, RemoteStoreMetadataAction.INSTANCE); } @Override public void remoteStoreMetadata(org.opensearch.action.admin.cluster.remotestore.metadata.RemoteStoreMetadataRequest request, org.opensearch.core.action.ActionListener listener) { - throw new UnsupportedOperationException("remoteStoreMetadata is not supported"); + execute(RemoteStoreMetadataAction.INSTANCE, request, listener); } } @@ -2087,83 +2095,83 @@ public SegmentReplicationStatsRequestBuilder prepareSegmentReplicationStats(fina @Override public void createView(org.opensearch.action.admin.indices.view.CreateViewAction.Request request, ActionListener listener) { - throw new UnsupportedOperationException("Not implemented yet"); + execute(CreateViewAction.INSTANCE, request, listener); } @Override public ActionFuture createView( org.opensearch.action.admin.indices.view.CreateViewAction.Request request) { - throw new UnsupportedOperationException("Not implemented yet"); + return execute(CreateViewAction.INSTANCE, request); } @Override public void getView(org.opensearch.action.admin.indices.view.GetViewAction.Request request, ActionListener listener) { - throw new UnsupportedOperationException("Not implemented yet"); + execute(GetViewAction.INSTANCE, request, listener); } @Override public ActionFuture getView( org.opensearch.action.admin.indices.view.GetViewAction.Request request) { - throw new UnsupportedOperationException("Not implemented yet"); + return execute(GetViewAction.INSTANCE, request); } @Override public void deleteView(org.opensearch.action.admin.indices.view.DeleteViewAction.Request request, ActionListener listener) { - throw new UnsupportedOperationException("Not implemented yet"); + execute(DeleteViewAction.INSTANCE, request, listener); } @Override public ActionFuture deleteView(org.opensearch.action.admin.indices.view.DeleteViewAction.Request request) { - throw new UnsupportedOperationException("Not implemented yet"); + return execute(DeleteViewAction.INSTANCE, request); } @Override public void updateView(org.opensearch.action.admin.indices.view.CreateViewAction.Request request, ActionListener listener) { - throw new UnsupportedOperationException("Not implemented yet"); + execute(UpdateViewAction.INSTANCE, request, listener); } @Override public ActionFuture updateView( org.opensearch.action.admin.indices.view.CreateViewAction.Request request) { - throw new UnsupportedOperationException("Not implemented yet"); + return execute(UpdateViewAction.INSTANCE, request); } @Override public ActionFuture pauseIngestion(PauseIngestionRequest request) { - throw new UnsupportedOperationException("Not implemented yet"); + return execute(PauseIngestionAction.INSTANCE, request); } @Override public void pauseIngestion(PauseIngestionRequest request, ActionListener listener) { - throw new UnsupportedOperationException("Not implemented yet"); + execute(PauseIngestionAction.INSTANCE, request, listener); } @Override public ActionFuture resumeIngestion(ResumeIngestionRequest request) { - throw new UnsupportedOperationException("Not implemented yet"); + return execute(ResumeIngestionAction.INSTANCE, request); } @Override public void resumeIngestion(ResumeIngestionRequest request, ActionListener listener) { - throw new UnsupportedOperationException("Not implemented yet"); + execute(ResumeIngestionAction.INSTANCE, request, listener); } @Override public ActionFuture getIngestionState(GetIngestionStateRequest request) { - throw new UnsupportedOperationException("Not implemented yet"); + return execute(GetIngestionStateAction.INSTANCE, request); } @Override public void getIngestionState(GetIngestionStateRequest request, ActionListener listener) { - throw new UnsupportedOperationException("Not implemented yet"); + execute(GetIngestionStateAction.INSTANCE, request, listener); } @Override public ScaleIndexRequestBuilder prepareScaleSearchOnly(String index, boolean searchOnly) { - throw new UnsupportedOperationException("Not implemented yet"); + return new ScaleIndexRequestBuilder(this, searchOnly, index); } } diff --git a/src/main/java/org/codelibs/fesen/client/HttpClient.java b/src/main/java/org/codelibs/fesen/client/HttpClient.java index 5d757e1..2f5ea08 100644 --- a/src/main/java/org/codelibs/fesen/client/HttpClient.java +++ b/src/main/java/org/codelibs/fesen/client/HttpClient.java @@ -105,6 +105,17 @@ import org.codelibs.fesen.client.action.HttpMultiTermVectorsAction; import org.codelibs.fesen.client.action.HttpNodesInfoAction; import org.codelibs.fesen.client.action.HttpNodesUsageAction; +import org.codelibs.fesen.client.action.HttpCreateViewAction; +import org.codelibs.fesen.client.action.HttpGetViewAction; +import org.codelibs.fesen.client.action.HttpDeleteViewAction; +import org.codelibs.fesen.client.action.HttpUpdateViewAction; +import org.codelibs.fesen.client.action.HttpSearchViewAction; +import org.codelibs.fesen.client.action.HttpListViewNamesAction; +import org.codelibs.fesen.client.action.HttpPauseIngestionAction; +import org.codelibs.fesen.client.action.HttpResumeIngestionAction; +import org.codelibs.fesen.client.action.HttpGetIngestionStateAction; +import org.codelibs.fesen.client.action.HttpScaleIndexAction; +import org.codelibs.fesen.client.action.HttpRemoteStoreMetadataAction; import org.codelibs.fesen.client.action.HttpRecoveryAction; import org.codelibs.fesen.client.action.HttpRemoteInfoAction; import org.codelibs.fesen.client.action.HttpTermVectorsAction; @@ -217,6 +228,25 @@ import org.opensearch.action.admin.cluster.wlm.WlmStatsAction; import org.opensearch.action.admin.cluster.wlm.WlmStatsRequest; import org.opensearch.action.admin.cluster.wlm.WlmStatsResponse; +import org.opensearch.action.admin.indices.view.CreateViewAction; +import org.opensearch.action.admin.indices.view.GetViewAction; +import org.opensearch.action.admin.indices.view.DeleteViewAction; +import org.opensearch.action.admin.indices.view.UpdateViewAction; +import org.opensearch.action.admin.indices.view.SearchViewAction; +import org.opensearch.action.admin.indices.view.ListViewNamesAction; +import org.opensearch.action.admin.indices.streamingingestion.pause.PauseIngestionAction; +import org.opensearch.action.admin.indices.streamingingestion.pause.PauseIngestionRequest; +import org.opensearch.action.admin.indices.streamingingestion.pause.PauseIngestionResponse; +import org.opensearch.action.admin.indices.streamingingestion.resume.ResumeIngestionAction; +import org.opensearch.action.admin.indices.streamingingestion.resume.ResumeIngestionRequest; +import org.opensearch.action.admin.indices.streamingingestion.resume.ResumeIngestionResponse; +import org.opensearch.action.admin.indices.streamingingestion.state.GetIngestionStateAction; +import org.opensearch.action.admin.indices.streamingingestion.state.GetIngestionStateRequest; +import org.opensearch.action.admin.indices.streamingingestion.state.GetIngestionStateResponse; +import org.opensearch.action.admin.indices.scale.searchonly.ScaleIndexAction; +import org.opensearch.action.admin.cluster.remotestore.metadata.RemoteStoreMetadataAction; +import org.opensearch.action.admin.cluster.remotestore.metadata.RemoteStoreMetadataRequest; +import org.opensearch.action.admin.cluster.remotestore.metadata.RemoteStoreMetadataResponse; import org.opensearch.action.admin.indices.alias.IndicesAliasesAction; import org.opensearch.action.admin.indices.alias.IndicesAliasesRequest; import org.opensearch.action.admin.indices.alias.get.GetAliasesAction; @@ -1018,6 +1048,57 @@ public HttpClient(final Settings settings, final ThreadPool threadPool, final Li new HttpMultiTermVectorsAction(this, MultiTermVectorsAction.INSTANCE).execute((MultiTermVectorsRequest) request, actionListener); }); + + // View API + actions.put(CreateViewAction.INSTANCE, (request, listener) -> { + new HttpCreateViewAction(this, CreateViewAction.INSTANCE).execute((CreateViewAction.Request) request, + (ActionListener) listener); + }); + actions.put(GetViewAction.INSTANCE, (request, listener) -> { + new HttpGetViewAction(this, GetViewAction.INSTANCE).execute((GetViewAction.Request) request, + (ActionListener) listener); + }); + actions.put(DeleteViewAction.INSTANCE, (request, listener) -> { + new HttpDeleteViewAction(this, DeleteViewAction.INSTANCE).execute((DeleteViewAction.Request) request, + (ActionListener) listener); + }); + actions.put(UpdateViewAction.INSTANCE, (request, listener) -> { + new HttpUpdateViewAction(this, UpdateViewAction.INSTANCE).execute((CreateViewAction.Request) request, + (ActionListener) listener); + }); + actions.put(SearchViewAction.INSTANCE, (request, listener) -> { + new HttpSearchViewAction(this, SearchViewAction.INSTANCE).execute((SearchViewAction.Request) request, + (ActionListener) listener); + }); + actions.put(ListViewNamesAction.INSTANCE, (request, listener) -> { + new HttpListViewNamesAction(this, ListViewNamesAction.INSTANCE).execute((ListViewNamesAction.Request) request, + (ActionListener) listener); + }); + + // Streaming Ingestion API + actions.put(PauseIngestionAction.INSTANCE, (request, listener) -> { + new HttpPauseIngestionAction(this, PauseIngestionAction.INSTANCE).execute((PauseIngestionRequest) request, + (ActionListener) listener); + }); + actions.put(ResumeIngestionAction.INSTANCE, (request, listener) -> { + new HttpResumeIngestionAction(this, ResumeIngestionAction.INSTANCE).execute((ResumeIngestionRequest) request, + (ActionListener) listener); + }); + actions.put(GetIngestionStateAction.INSTANCE, (request, listener) -> { + new HttpGetIngestionStateAction(this, GetIngestionStateAction.INSTANCE).execute((GetIngestionStateRequest) request, + (ActionListener) listener); + }); + + // Scale (Search-Only) API + actions.put(ScaleIndexAction.INSTANCE, (request, listener) -> { + new HttpScaleIndexAction(this, ScaleIndexAction.INSTANCE).execute(request, (ActionListener) listener); + }); + + // Remote Store Metadata API + actions.put(RemoteStoreMetadataAction.INSTANCE, (request, listener) -> { + new HttpRemoteStoreMetadataAction(this, RemoteStoreMetadataAction.INSTANCE).execute((RemoteStoreMetadataRequest) request, + (ActionListener) listener); + }); } @Override @@ -1288,29 +1369,29 @@ protected WorkerThread(final ForkJoinPool pool) { @Override public void searchView(org.opensearch.action.admin.indices.view.SearchViewAction.Request request, ActionListener listener) { - throw new UnsupportedOperationException("Not implemented yet"); + execute(SearchViewAction.INSTANCE, request, listener); } @Override public ActionFuture searchView(org.opensearch.action.admin.indices.view.SearchViewAction.Request request) { - throw new UnsupportedOperationException("Not implemented yet"); + return execute(SearchViewAction.INSTANCE, request); } @Override public void listViewNames(org.opensearch.action.admin.indices.view.ListViewNamesAction.Request request, ActionListener listener) { - throw new UnsupportedOperationException("Not implemented yet"); + execute(ListViewNamesAction.INSTANCE, request, listener); } @Override public ActionFuture listViewNames( org.opensearch.action.admin.indices.view.ListViewNamesAction.Request request) { - throw new UnsupportedOperationException("Not implemented yet"); + return execute(ListViewNamesAction.INSTANCE, request); } @Override public SearchRequestBuilder prepareStreamSearch(final String... indices) { - throw new UnsupportedOperationException("prepareStreamSearch is not supported"); + return new SearchRequestBuilder(this, SearchAction.INSTANCE).setIndices(indices); } } diff --git a/src/main/java/org/codelibs/fesen/client/HttpIndicesAdminClient.java b/src/main/java/org/codelibs/fesen/client/HttpIndicesAdminClient.java index f56fffd..d4bec5b 100644 --- a/src/main/java/org/codelibs/fesen/client/HttpIndicesAdminClient.java +++ b/src/main/java/org/codelibs/fesen/client/HttpIndicesAdminClient.java @@ -686,82 +686,82 @@ public SegmentReplicationStatsRequestBuilder prepareSegmentReplicationStats(fina @Override public void createView(org.opensearch.action.admin.indices.view.CreateViewAction.Request request, ActionListener listener) { - throw new UnsupportedOperationException("Not implemented yet"); + indicesClient.createView(request, listener); } @Override public ActionFuture createView( org.opensearch.action.admin.indices.view.CreateViewAction.Request request) { - throw new UnsupportedOperationException("Not implemented yet"); + return indicesClient.createView(request); } @Override public void getView(org.opensearch.action.admin.indices.view.GetViewAction.Request request, ActionListener listener) { - throw new UnsupportedOperationException("Not implemented yet"); + indicesClient.getView(request, listener); } @Override public ActionFuture getView( org.opensearch.action.admin.indices.view.GetViewAction.Request request) { - throw new UnsupportedOperationException("Not implemented yet"); + return indicesClient.getView(request); } @Override public void deleteView(org.opensearch.action.admin.indices.view.DeleteViewAction.Request request, ActionListener listener) { - throw new UnsupportedOperationException("Not implemented yet"); + indicesClient.deleteView(request, listener); } @Override public ActionFuture deleteView(org.opensearch.action.admin.indices.view.DeleteViewAction.Request request) { - throw new UnsupportedOperationException("Not implemented yet"); + return indicesClient.deleteView(request); } @Override public void updateView(org.opensearch.action.admin.indices.view.CreateViewAction.Request request, ActionListener listener) { - throw new UnsupportedOperationException("Not implemented yet"); + indicesClient.updateView(request, listener); } @Override public ActionFuture updateView( org.opensearch.action.admin.indices.view.CreateViewAction.Request request) { - throw new UnsupportedOperationException("Not implemented yet"); + return indicesClient.updateView(request); } @Override public ActionFuture pauseIngestion(PauseIngestionRequest request) { - throw new UnsupportedOperationException("Not implemented yet"); + return indicesClient.pauseIngestion(request); } @Override public void pauseIngestion(PauseIngestionRequest request, ActionListener listener) { - throw new UnsupportedOperationException("Not implemented yet"); + indicesClient.pauseIngestion(request, listener); } @Override public ActionFuture resumeIngestion(ResumeIngestionRequest request) { - throw new UnsupportedOperationException("Not implemented yet"); + return indicesClient.resumeIngestion(request); } @Override public void resumeIngestion(ResumeIngestionRequest request, ActionListener listener) { - throw new UnsupportedOperationException("Not implemented yet"); + indicesClient.resumeIngestion(request, listener); } @Override public ActionFuture getIngestionState(GetIngestionStateRequest request) { - throw new UnsupportedOperationException("Not implemented yet"); + return indicesClient.getIngestionState(request); } @Override public void getIngestionState(GetIngestionStateRequest request, ActionListener listener) { - throw new UnsupportedOperationException("Not implemented yet"); + indicesClient.getIngestionState(request, listener); } @Override public ScaleIndexRequestBuilder prepareScaleSearchOnly(String index, boolean searchOnly) { - throw new UnsupportedOperationException("Not implemented yet"); + return indicesClient.prepareScaleSearchOnly(index, searchOnly); } } diff --git a/src/main/java/org/codelibs/fesen/client/action/HttpCreateIndexAction.java b/src/main/java/org/codelibs/fesen/client/action/HttpCreateIndexAction.java index dfa5b63..ce73353 100644 --- a/src/main/java/org/codelibs/fesen/client/action/HttpCreateIndexAction.java +++ b/src/main/java/org/codelibs/fesen/client/action/HttpCreateIndexAction.java @@ -83,20 +83,19 @@ protected XContentBuilder innerToXContent(final CreateIndexRequest request, fina builder.endObject(); final String mappingSource = request.mappings(); - if (mappingSource == null) { - throw new UnsupportedOperationException("unknown mapping operation."); - } - try (final XContentParser createParser = - JsonXContent.jsonXContent.createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, mappingSource)) { - Map mappingMap = createParser.map(); - if (mappingMap.get("_doc") instanceof final Map map) { - mappingMap = map; - } - builder.startObject(MAPPINGS.getPreferredName()); - for (final Map.Entry e : mappingMap.entrySet()) { - builder.field(e.getKey(), e.getValue()); + if (mappingSource != null) { + try (final XContentParser createParser = JsonXContent.jsonXContent.createParser(NamedXContentRegistry.EMPTY, + LoggingDeprecationHandler.INSTANCE, mappingSource)) { + Map mappingMap = createParser.map(); + if (mappingMap.get("_doc") instanceof final Map map) { + mappingMap = map; + } + builder.startObject(MAPPINGS.getPreferredName()); + for (final Map.Entry e : mappingMap.entrySet()) { + builder.field(e.getKey(), e.getValue()); + } + builder.endObject(); } - builder.endObject(); } builder.startObject(ALIASES.getPreferredName()); diff --git a/src/main/java/org/codelibs/fesen/client/action/HttpCreateViewAction.java b/src/main/java/org/codelibs/fesen/client/action/HttpCreateViewAction.java new file mode 100644 index 0000000..52bad86 --- /dev/null +++ b/src/main/java/org/codelibs/fesen/client/action/HttpCreateViewAction.java @@ -0,0 +1,73 @@ +/* + * Copyright 2012-2025 CodeLibs Project and the Others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.codelibs.fesen.client.action; + +import java.io.IOException; + +import org.codelibs.curl.CurlRequest; +import org.codelibs.fesen.client.HttpClient; +import org.opensearch.OpenSearchException; +import org.opensearch.action.admin.indices.view.CreateViewAction; +import org.opensearch.action.admin.indices.view.GetViewAction; +import org.opensearch.common.xcontent.json.JsonXContent; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.common.bytes.BytesReference; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.core.xcontent.XContentParser; + +public class HttpCreateViewAction extends HttpAction { + + protected final CreateViewAction action; + + public HttpCreateViewAction(final HttpClient client, final CreateViewAction action) { + super(client); + this.action = action; + } + + public void execute(final CreateViewAction.Request request, final ActionListener listener) { + String source = null; + try (final XContentBuilder builder = JsonXContent.contentBuilder()) { + builder.startObject(); + builder.field("name", request.getName()); + builder.field("description", request.getDescription()); + builder.startArray("targets"); + for (final CreateViewAction.Request.Target target : request.getTargets()) { + builder.startObject(); + builder.field("index_pattern", target.getIndexPattern()); + builder.endObject(); + } + builder.endArray(); + builder.endObject(); + builder.flush(); + source = BytesReference.bytes(builder).utf8ToString(); + } catch (final IOException e) { + throw new OpenSearchException("Failed to parse a request.", e); + } + getCurlRequest(request).body(source).execute(response -> { + try (final XContentParser parser = createParser(response)) { + final GetViewAction.Response getViewResponse = GetViewAction.Response.fromXContent(parser); + listener.onResponse(getViewResponse); + } catch (final Exception e) { + listener.onFailure(toOpenSearchException(response, e)); + } + }, e -> unwrapOpenSearchException(listener, e)); + } + + protected CurlRequest getCurlRequest(final CreateViewAction.Request request) { + // RestViewAction + return client.getCurlRequest(POST, "/views"); + } +} diff --git a/src/main/java/org/codelibs/fesen/client/action/HttpDeleteViewAction.java b/src/main/java/org/codelibs/fesen/client/action/HttpDeleteViewAction.java new file mode 100644 index 0000000..f7254f4 --- /dev/null +++ b/src/main/java/org/codelibs/fesen/client/action/HttpDeleteViewAction.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2025 CodeLibs Project and the Others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.codelibs.fesen.client.action; + +import org.codelibs.curl.CurlRequest; +import org.codelibs.fesen.client.HttpClient; +import org.codelibs.fesen.client.util.UrlUtils; +import org.opensearch.action.admin.indices.view.DeleteViewAction; +import org.opensearch.action.support.clustermanager.AcknowledgedResponse; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.xcontent.XContentParser; + +public class HttpDeleteViewAction extends HttpAction { + + protected final DeleteViewAction action; + + public HttpDeleteViewAction(final HttpClient client, final DeleteViewAction action) { + super(client); + this.action = action; + } + + public void execute(final DeleteViewAction.Request request, final ActionListener listener) { + getCurlRequest(request).execute(response -> { + try (final XContentParser parser = createParser(response)) { + final AcknowledgedResponse deleteViewResponse = AcknowledgedResponse.fromXContent(parser); + listener.onResponse(deleteViewResponse); + } catch (final Exception e) { + listener.onFailure(toOpenSearchException(response, e)); + } + }, e -> unwrapOpenSearchException(listener, e)); + } + + protected CurlRequest getCurlRequest(final DeleteViewAction.Request request) { + // RestViewAction + return client.getCurlRequest(DELETE, "/views/" + UrlUtils.encode(request.getName())); + } +} diff --git a/src/main/java/org/codelibs/fesen/client/action/HttpGetIngestionStateAction.java b/src/main/java/org/codelibs/fesen/client/action/HttpGetIngestionStateAction.java new file mode 100644 index 0000000..853ca1d --- /dev/null +++ b/src/main/java/org/codelibs/fesen/client/action/HttpGetIngestionStateAction.java @@ -0,0 +1,119 @@ +/* + * Copyright 2012-2025 CodeLibs Project and the Others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.codelibs.fesen.client.action; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.codelibs.curl.CurlRequest; +import org.codelibs.fesen.client.HttpClient; +import org.opensearch.action.admin.indices.streamingingestion.state.GetIngestionStateAction; +import org.opensearch.action.admin.indices.streamingingestion.state.GetIngestionStateRequest; +import org.opensearch.action.admin.indices.streamingingestion.state.GetIngestionStateResponse; +import org.opensearch.action.admin.indices.streamingingestion.state.ShardIngestionState; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.action.support.DefaultShardOperationFailedException; +import org.opensearch.core.xcontent.XContentParser; + +public class HttpGetIngestionStateAction extends HttpAction { + + protected final GetIngestionStateAction action; + + public HttpGetIngestionStateAction(final HttpClient client, final GetIngestionStateAction action) { + super(client); + this.action = action; + } + + public void execute(final GetIngestionStateRequest request, final ActionListener listener) { + getCurlRequest(request).execute(response -> { + try (final XContentParser parser = createParser(response)) { + final GetIngestionStateResponse getIngestionStateResponse = fromXContent(parser); + listener.onResponse(getIngestionStateResponse); + } catch (final Exception e) { + listener.onFailure(toOpenSearchException(response, e)); + } + }, e -> unwrapOpenSearchException(listener, e)); + } + + protected GetIngestionStateResponse fromXContent(final XContentParser parser) throws IOException { + String fieldName = null; + int totalShards = 0; + int successfulShards = 0; + int failedShards = 0; + final List shardFailures = new ArrayList<>(); + + XContentParser.Token token = parser.nextToken(); + if (token != XContentParser.Token.START_OBJECT) { + throw new IOException("Expected START_OBJECT but got " + token); + } + + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + fieldName = parser.currentName(); + } else if (token == XContentParser.Token.START_OBJECT) { + if ("_shards".equals(fieldName)) { + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + fieldName = parser.currentName(); + } else if (token == XContentParser.Token.VALUE_NUMBER) { + if ("total".equals(fieldName)) { + totalShards = parser.intValue(); + } else if ("successful".equals(fieldName)) { + successfulShards = parser.intValue(); + } else if ("failed".equals(fieldName)) { + failedShards = parser.intValue(); + } + } + } + } else { + consumeObject(parser); + } + } else if (token == XContentParser.Token.START_ARRAY) { + consumeObject(parser); + } + } + + return new GetIngestionStateResponse(new ShardIngestionState[0], totalShards, successfulShards, failedShards, null, shardFailures); + } + + protected void consumeObject(final XContentParser parser) throws IOException { + XContentParser.Token token; + int depth = 1; + while (depth > 0) { + token = parser.nextToken(); + if (token == XContentParser.Token.START_OBJECT || token == XContentParser.Token.START_ARRAY) { + depth++; + } else if (token == XContentParser.Token.END_OBJECT || token == XContentParser.Token.END_ARRAY) { + depth--; + } + } + } + + protected CurlRequest getCurlRequest(final GetIngestionStateRequest request) { + // RestGetIngestionStateAction + final CurlRequest curlRequest = client.getCurlRequest(GET, "/ingestion/_state", request.indices()); + if (request.getShards().length > 0) { + curlRequest.param("shards", IntStream.of(request.getShards()).mapToObj(String::valueOf).collect(Collectors.joining(","))); + } + if (request.timeout() != null) { + curlRequest.param("timeout", request.timeout().toString()); + } + return curlRequest; + } +} diff --git a/src/main/java/org/codelibs/fesen/client/action/HttpGetViewAction.java b/src/main/java/org/codelibs/fesen/client/action/HttpGetViewAction.java new file mode 100644 index 0000000..38a0c1b --- /dev/null +++ b/src/main/java/org/codelibs/fesen/client/action/HttpGetViewAction.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2025 CodeLibs Project and the Others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.codelibs.fesen.client.action; + +import org.codelibs.curl.CurlRequest; +import org.codelibs.fesen.client.HttpClient; +import org.codelibs.fesen.client.util.UrlUtils; +import org.opensearch.action.admin.indices.view.GetViewAction; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.xcontent.XContentParser; + +public class HttpGetViewAction extends HttpAction { + + protected final GetViewAction action; + + public HttpGetViewAction(final HttpClient client, final GetViewAction action) { + super(client); + this.action = action; + } + + public void execute(final GetViewAction.Request request, final ActionListener listener) { + getCurlRequest(request).execute(response -> { + try (final XContentParser parser = createParser(response)) { + final GetViewAction.Response getViewResponse = GetViewAction.Response.fromXContent(parser); + listener.onResponse(getViewResponse); + } catch (final Exception e) { + listener.onFailure(toOpenSearchException(response, e)); + } + }, e -> unwrapOpenSearchException(listener, e)); + } + + protected CurlRequest getCurlRequest(final GetViewAction.Request request) { + // RestViewAction + return client.getCurlRequest(GET, "/views/" + UrlUtils.encode(request.getName())); + } +} diff --git a/src/main/java/org/codelibs/fesen/client/action/HttpListViewNamesAction.java b/src/main/java/org/codelibs/fesen/client/action/HttpListViewNamesAction.java new file mode 100644 index 0000000..99468ce --- /dev/null +++ b/src/main/java/org/codelibs/fesen/client/action/HttpListViewNamesAction.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2025 CodeLibs Project and the Others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.codelibs.fesen.client.action; + +import java.util.ArrayList; +import java.util.List; + +import org.codelibs.curl.CurlRequest; +import org.codelibs.fesen.client.HttpClient; +import org.opensearch.action.admin.indices.view.ListViewNamesAction; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.xcontent.XContentParser; + +public class HttpListViewNamesAction extends HttpAction { + + protected final ListViewNamesAction action; + + public HttpListViewNamesAction(final HttpClient client, final ListViewNamesAction action) { + super(client); + this.action = action; + } + + public void execute(final ListViewNamesAction.Request request, final ActionListener listener) { + getCurlRequest(request).execute(response -> { + try (final XContentParser parser = createParser(response)) { + final List viewNames = new ArrayList<>(); + XContentParser.Token token = parser.nextToken(); + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME && "views".equals(parser.currentName())) { + parser.nextToken(); // START_ARRAY + while (parser.nextToken() != XContentParser.Token.END_ARRAY) { + viewNames.add(parser.text()); + } + } + } + listener.onResponse(new ListViewNamesAction.Response(viewNames)); + } catch (final Exception e) { + listener.onFailure(toOpenSearchException(response, e)); + } + }, e -> unwrapOpenSearchException(listener, e)); + } + + protected CurlRequest getCurlRequest(final ListViewNamesAction.Request request) { + // RestViewAction + return client.getCurlRequest(GET, "/views/"); + } +} diff --git a/src/main/java/org/codelibs/fesen/client/action/HttpPauseIngestionAction.java b/src/main/java/org/codelibs/fesen/client/action/HttpPauseIngestionAction.java new file mode 100644 index 0000000..7e5e95b --- /dev/null +++ b/src/main/java/org/codelibs/fesen/client/action/HttpPauseIngestionAction.java @@ -0,0 +1,99 @@ +/* + * Copyright 2012-2025 CodeLibs Project and the Others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.codelibs.fesen.client.action; + +import java.io.IOException; + +import org.codelibs.curl.CurlRequest; +import org.codelibs.fesen.client.HttpClient; +import org.opensearch.action.admin.indices.streamingingestion.IngestionStateShardFailure; +import org.opensearch.action.admin.indices.streamingingestion.pause.PauseIngestionAction; +import org.opensearch.action.admin.indices.streamingingestion.pause.PauseIngestionRequest; +import org.opensearch.action.admin.indices.streamingingestion.pause.PauseIngestionResponse; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.xcontent.XContentParser; + +public class HttpPauseIngestionAction extends HttpAction { + + protected final PauseIngestionAction action; + + public HttpPauseIngestionAction(final HttpClient client, final PauseIngestionAction action) { + super(client); + this.action = action; + } + + public void execute(final PauseIngestionRequest request, final ActionListener listener) { + getCurlRequest(request).execute(response -> { + try (final XContentParser parser = createParser(response)) { + final PauseIngestionResponse pauseIngestionResponse = fromXContent(parser); + listener.onResponse(pauseIngestionResponse); + } catch (final Exception e) { + listener.onFailure(toOpenSearchException(response, e)); + } + }, e -> unwrapOpenSearchException(listener, e)); + } + + protected PauseIngestionResponse fromXContent(final XContentParser parser) throws IOException { + boolean acknowledged = false; + boolean shardsAcknowledged = false; + + XContentParser.Token token = parser.nextToken(); + if (token != XContentParser.Token.START_OBJECT) { + throw new IOException("Expected START_OBJECT but got " + token); + } + + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + final String field = parser.currentName(); + parser.nextToken(); + if ("acknowledged".equals(field)) { + acknowledged = parser.booleanValue(); + } else if ("shards_acknowledged".equals(field)) { + shardsAcknowledged = parser.booleanValue(); + } else if (token == XContentParser.Token.START_OBJECT || token == XContentParser.Token.START_ARRAY) { + consumeObject(parser); + } + } + } + + return new PauseIngestionResponse(acknowledged, shardsAcknowledged, new IngestionStateShardFailure[0], ""); + } + + protected void consumeObject(final XContentParser parser) throws IOException { + XContentParser.Token token; + int depth = 1; + while (depth > 0) { + token = parser.nextToken(); + if (token == XContentParser.Token.START_OBJECT || token == XContentParser.Token.START_ARRAY) { + depth++; + } else if (token == XContentParser.Token.END_OBJECT || token == XContentParser.Token.END_ARRAY) { + depth--; + } + } + } + + protected CurlRequest getCurlRequest(final PauseIngestionRequest request) { + // RestPauseIngestionAction + final CurlRequest curlRequest = client.getCurlRequest(POST, "/ingestion/_pause", request.indices()); + if (request.timeout() != null) { + curlRequest.param("timeout", request.timeout().toString()); + } + if (request.clusterManagerNodeTimeout() != null) { + curlRequest.param("cluster_manager_timeout", request.clusterManagerNodeTimeout().toString()); + } + return curlRequest; + } +} diff --git a/src/main/java/org/codelibs/fesen/client/action/HttpRemoteStoreMetadataAction.java b/src/main/java/org/codelibs/fesen/client/action/HttpRemoteStoreMetadataAction.java new file mode 100644 index 0000000..e19fef2 --- /dev/null +++ b/src/main/java/org/codelibs/fesen/client/action/HttpRemoteStoreMetadataAction.java @@ -0,0 +1,123 @@ +/* + * Copyright 2012-2025 CodeLibs Project and the Others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.codelibs.fesen.client.action; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.codelibs.curl.CurlRequest; +import org.codelibs.fesen.client.HttpClient; +import org.codelibs.fesen.client.util.UrlUtils; +import org.opensearch.action.admin.cluster.remotestore.metadata.RemoteStoreMetadataAction; +import org.opensearch.action.admin.cluster.remotestore.metadata.RemoteStoreMetadataRequest; +import org.opensearch.action.admin.cluster.remotestore.metadata.RemoteStoreMetadataResponse; +import org.opensearch.action.admin.cluster.remotestore.metadata.RemoteStoreShardMetadata; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.action.support.DefaultShardOperationFailedException; +import org.opensearch.core.xcontent.XContentParser; + +public class HttpRemoteStoreMetadataAction extends HttpAction { + + protected RemoteStoreMetadataAction action; + + public HttpRemoteStoreMetadataAction(final HttpClient client, final RemoteStoreMetadataAction action) { + super(client); + this.action = action; + } + + public void execute(final RemoteStoreMetadataRequest request, final ActionListener listener) { + getCurlRequest(request).execute(response -> { + try (final XContentParser parser = createParser(response)) { + final RemoteStoreMetadataResponse remoteStoreMetadataResponse = fromXContent(parser); + listener.onResponse(remoteStoreMetadataResponse); + } catch (final Exception e) { + listener.onFailure(toOpenSearchException(response, e)); + } + }, e -> unwrapOpenSearchException(listener, e)); + } + + protected RemoteStoreMetadataResponse fromXContent(final XContentParser parser) throws IOException { + String fieldName = null; + int totalShards = 0; + int successfulShards = 0; + int failedShards = 0; + final List shardFailures = new ArrayList<>(); + + // Initialize parser - move to START_OBJECT + XContentParser.Token token = parser.nextToken(); + if (token != XContentParser.Token.START_OBJECT) { + throw new IOException("Expected START_OBJECT but got " + token); + } + + // Move to first field or END_OBJECT + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + fieldName = parser.currentName(); + } else if (token == XContentParser.Token.START_OBJECT) { + if ("_shards".equals(fieldName)) { + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + fieldName = parser.currentName(); + } else if (token == XContentParser.Token.VALUE_NUMBER) { + if ("total".equals(fieldName)) { + totalShards = parser.intValue(); + } else if ("successful".equals(fieldName)) { + successfulShards = parser.intValue(); + } else if ("failed".equals(fieldName)) { + failedShards = parser.intValue(); + } + } + } + } else if ("indices".equals(fieldName)) { + // Skip indices section - complex to parse + consumeObject(parser); + } else { + consumeObject(parser); + } + } + } + + // Build RemoteStoreMetadataResponse with empty RemoteStoreShardMetadata array + return new RemoteStoreMetadataResponse(new RemoteStoreShardMetadata[0], totalShards, successfulShards, failedShards, shardFailures); + } + + protected void consumeObject(final XContentParser parser) throws IOException { + XContentParser.Token token; + int depth = 1; + while (depth > 0) { + token = parser.nextToken(); + if (token == XContentParser.Token.START_OBJECT || token == XContentParser.Token.START_ARRAY) { + depth++; + } else if (token == XContentParser.Token.END_OBJECT || token == XContentParser.Token.END_ARRAY) { + depth--; + } + } + } + + protected CurlRequest getCurlRequest(final RemoteStoreMetadataRequest request) { + final StringBuilder buf = new StringBuilder(); + buf.append("/_remotestore/metadata"); + if (request.indices() != null && request.indices().length > 0) { + buf.append('/').append(UrlUtils.joinAndEncode(",", request.indices())); + } + if (request.shards() != null && request.shards().length > 0) { + buf.append('/').append(String.join(",", request.shards())); + } + final CurlRequest curlRequest = client.getCurlRequest(GET, buf.toString()); + return curlRequest; + } +} diff --git a/src/main/java/org/codelibs/fesen/client/action/HttpResumeIngestionAction.java b/src/main/java/org/codelibs/fesen/client/action/HttpResumeIngestionAction.java new file mode 100644 index 0000000..abdeddd --- /dev/null +++ b/src/main/java/org/codelibs/fesen/client/action/HttpResumeIngestionAction.java @@ -0,0 +1,128 @@ +/* + * Copyright 2012-2025 CodeLibs Project and the Others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.codelibs.fesen.client.action; + +import java.io.IOException; +import java.util.Locale; + +import org.codelibs.curl.CurlRequest; +import org.codelibs.fesen.client.HttpClient; +import org.opensearch.OpenSearchException; +import org.opensearch.action.admin.indices.streamingingestion.IngestionStateShardFailure; +import org.opensearch.action.admin.indices.streamingingestion.resume.ResumeIngestionAction; +import org.opensearch.action.admin.indices.streamingingestion.resume.ResumeIngestionRequest; +import org.opensearch.action.admin.indices.streamingingestion.resume.ResumeIngestionResponse; +import org.opensearch.common.xcontent.json.JsonXContent; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.common.bytes.BytesReference; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.core.xcontent.XContentParser; + +public class HttpResumeIngestionAction extends HttpAction { + + protected final ResumeIngestionAction action; + + public HttpResumeIngestionAction(final HttpClient client, final ResumeIngestionAction action) { + super(client); + this.action = action; + } + + public void execute(final ResumeIngestionRequest request, final ActionListener listener) { + String body = null; + if (request.getResetSettings().length > 0) { + try (final XContentBuilder builder = JsonXContent.contentBuilder()) { + builder.startObject(); + builder.startArray("reset_settings"); + for (final ResumeIngestionRequest.ResetSettings rs : request.getResetSettings()) { + builder.startObject(); + builder.field("shard", rs.getShard()); + builder.field("mode", rs.getMode().name().toLowerCase(Locale.ROOT)); + builder.field("value", rs.getValue()); + builder.endObject(); + } + builder.endArray(); + builder.endObject(); + builder.flush(); + body = BytesReference.bytes(builder).utf8ToString(); + } catch (final IOException e) { + throw new OpenSearchException("Failed to parse a request.", e); + } + } + final CurlRequest curlRequest = getCurlRequest(request); + if (body != null) { + curlRequest.body(body); + } + curlRequest.execute(response -> { + try (final XContentParser parser = createParser(response)) { + final ResumeIngestionResponse resumeIngestionResponse = fromXContent(parser); + listener.onResponse(resumeIngestionResponse); + } catch (final Exception e) { + listener.onFailure(toOpenSearchException(response, e)); + } + }, e -> unwrapOpenSearchException(listener, e)); + } + + protected ResumeIngestionResponse fromXContent(final XContentParser parser) throws IOException { + boolean acknowledged = false; + boolean shardsAcknowledged = false; + + XContentParser.Token token = parser.nextToken(); + if (token != XContentParser.Token.START_OBJECT) { + throw new IOException("Expected START_OBJECT but got " + token); + } + + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + final String field = parser.currentName(); + parser.nextToken(); + if ("acknowledged".equals(field)) { + acknowledged = parser.booleanValue(); + } else if ("shards_acknowledged".equals(field)) { + shardsAcknowledged = parser.booleanValue(); + } else if (token == XContentParser.Token.START_OBJECT || token == XContentParser.Token.START_ARRAY) { + consumeObject(parser); + } + } + } + + return new ResumeIngestionResponse(acknowledged, shardsAcknowledged, new IngestionStateShardFailure[0], ""); + } + + protected void consumeObject(final XContentParser parser) throws IOException { + XContentParser.Token token; + int depth = 1; + while (depth > 0) { + token = parser.nextToken(); + if (token == XContentParser.Token.START_OBJECT || token == XContentParser.Token.START_ARRAY) { + depth++; + } else if (token == XContentParser.Token.END_OBJECT || token == XContentParser.Token.END_ARRAY) { + depth--; + } + } + } + + protected CurlRequest getCurlRequest(final ResumeIngestionRequest request) { + // RestResumeIngestionAction + final CurlRequest curlRequest = client.getCurlRequest(POST, "/ingestion/_resume", request.indices()); + if (request.timeout() != null) { + curlRequest.param("timeout", request.timeout().toString()); + } + if (request.clusterManagerNodeTimeout() != null) { + curlRequest.param("cluster_manager_timeout", request.clusterManagerNodeTimeout().toString()); + } + return curlRequest; + } +} diff --git a/src/main/java/org/codelibs/fesen/client/action/HttpScaleIndexAction.java b/src/main/java/org/codelibs/fesen/client/action/HttpScaleIndexAction.java new file mode 100644 index 0000000..57c4a57 --- /dev/null +++ b/src/main/java/org/codelibs/fesen/client/action/HttpScaleIndexAction.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2025 CodeLibs Project and the Others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.codelibs.fesen.client.action; + +import java.lang.reflect.Method; + +import org.codelibs.curl.CurlRequest; +import org.codelibs.fesen.client.HttpClient; +import org.opensearch.OpenSearchException; +import org.opensearch.action.ActionRequest; +import org.opensearch.action.admin.indices.scale.searchonly.ScaleIndexAction; +import org.opensearch.action.support.clustermanager.AcknowledgedResponse; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.xcontent.XContentParser; + +public class HttpScaleIndexAction extends HttpAction { + + protected final ScaleIndexAction action; + + public HttpScaleIndexAction(final HttpClient client, final ScaleIndexAction action) { + super(client); + this.action = action; + } + + public void execute(final ActionRequest request, final ActionListener listener) { + // Use reflection to access package-private ScaleIndexRequest fields + try { + final Method getIndexMethod = request.getClass().getMethod("getIndex"); + final Method isScaleDownMethod = request.getClass().getMethod("isScaleDown"); + final String index = (String) getIndexMethod.invoke(request); + final boolean scaleDown = (boolean) isScaleDownMethod.invoke(request); + + final String body = "{\"search_only\":" + scaleDown + "}"; + final CurlRequest curlRequest = client.getCurlRequest(POST, "/_scale", index); + curlRequest.body(body).execute(response -> { + try (final XContentParser parser = createParser(response)) { + final AcknowledgedResponse ackResponse = AcknowledgedResponse.fromXContent(parser); + listener.onResponse(ackResponse); + } catch (final Exception e) { + listener.onFailure(toOpenSearchException(response, e)); + } + }, e -> unwrapOpenSearchException(listener, e)); + } catch (final Exception e) { + listener.onFailure(new OpenSearchException("Failed to execute scale index action", e)); + } + } +} diff --git a/src/main/java/org/codelibs/fesen/client/action/HttpSearchViewAction.java b/src/main/java/org/codelibs/fesen/client/action/HttpSearchViewAction.java new file mode 100644 index 0000000..b6c6f8e --- /dev/null +++ b/src/main/java/org/codelibs/fesen/client/action/HttpSearchViewAction.java @@ -0,0 +1,102 @@ +/* + * Copyright 2012-2025 CodeLibs Project and the Others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.codelibs.fesen.client.action; + +import java.io.IOException; + +import org.codelibs.curl.CurlRequest; +import org.codelibs.fesen.client.HttpClient; +import org.codelibs.fesen.client.util.UrlUtils; +import org.opensearch.OpenSearchException; +import org.opensearch.action.admin.indices.view.SearchViewAction; +import org.opensearch.action.search.SearchResponse; +import org.opensearch.action.search.SearchType; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.core.xcontent.XContentHelper; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.search.builder.SearchSourceBuilder; + +public class HttpSearchViewAction extends HttpAction { + + protected final SearchViewAction action; + + public HttpSearchViewAction(final HttpClient client, final SearchViewAction action) { + super(client); + this.action = action; + } + + public void execute(final SearchViewAction.Request request, final ActionListener listener) { + getCurlRequest(request).body(getQuerySource(request)).execute(response -> { + try (final XContentParser parser = createParser(response)) { + final SearchResponse searchResponse = SearchResponse.fromXContent(parser); + if (searchResponse.getHits() == null) { + listener.onFailure(toOpenSearchException(response, new OpenSearchException("hits is null."))); + } else { + listener.onResponse(searchResponse); + } + } catch (final Exception e) { + listener.onFailure(toOpenSearchException(response, e)); + } + }, e -> unwrapOpenSearchException(listener, e)); + } + + protected String getQuerySource(final SearchViewAction.Request request) { + final SearchSourceBuilder source = request.source(); + if (source != null) { + try { + return XContentHelper.toXContent(source, XContentType.JSON, ToXContent.EMPTY_PARAMS, false).utf8ToString(); + } catch (final IOException e) { + throw new OpenSearchException(e); + } + } + return null; + } + + protected CurlRequest getCurlRequest(final SearchViewAction.Request request) { + // RestViewAction + final CurlRequest curlRequest = client.getCurlRequest(POST, "/views/" + UrlUtils.encode(request.getView()) + "/_search"); + curlRequest.param("typed_keys", "true"); + curlRequest.param("batched_reduce_size", Integer.toString(request.getBatchedReduceSize())); + if (request.getPreFilterShardSize() != null) { + curlRequest.param("pre_filter_shard_size", request.getPreFilterShardSize().toString()); + } + if (request.getMaxConcurrentShardRequests() > 0) { + curlRequest.param("max_concurrent_shard_requests", Integer.toString(request.getMaxConcurrentShardRequests())); + } + if (request.allowPartialSearchResults() != null) { + curlRequest.param("allow_partial_search_results", request.allowPartialSearchResults().toString()); + } + if (!SearchType.DEFAULT.equals(request.searchType())) { + curlRequest.param("search_type", request.searchType().name().toLowerCase()); + } + if (request.requestCache() != null) { + curlRequest.param("request_cache", request.requestCache().toString()); + } + if (request.scroll() != null) { + curlRequest.param("scroll", request.scroll().keepAlive().toString()); + } + if (request.routing() != null) { + curlRequest.param("routing", request.routing()); + } + if (request.preference() != null) { + curlRequest.param("preference", request.preference()); + } + curlRequest.param("ccs_minimize_roundtrips", Boolean.toString(request.isCcsMinimizeRoundtrips())); + return curlRequest; + } +} diff --git a/src/main/java/org/codelibs/fesen/client/action/HttpUpdateViewAction.java b/src/main/java/org/codelibs/fesen/client/action/HttpUpdateViewAction.java new file mode 100644 index 0000000..45d5095 --- /dev/null +++ b/src/main/java/org/codelibs/fesen/client/action/HttpUpdateViewAction.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-2025 CodeLibs Project and the Others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.codelibs.fesen.client.action; + +import java.io.IOException; + +import org.codelibs.curl.CurlRequest; +import org.codelibs.fesen.client.HttpClient; +import org.codelibs.fesen.client.util.UrlUtils; +import org.opensearch.OpenSearchException; +import org.opensearch.action.admin.indices.view.CreateViewAction; +import org.opensearch.action.admin.indices.view.GetViewAction; +import org.opensearch.action.admin.indices.view.UpdateViewAction; +import org.opensearch.common.xcontent.json.JsonXContent; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.common.bytes.BytesReference; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.core.xcontent.XContentParser; + +public class HttpUpdateViewAction extends HttpAction { + + protected final UpdateViewAction action; + + public HttpUpdateViewAction(final HttpClient client, final UpdateViewAction action) { + super(client); + this.action = action; + } + + public void execute(final CreateViewAction.Request request, final ActionListener listener) { + String source = null; + try (final XContentBuilder builder = JsonXContent.contentBuilder()) { + builder.startObject(); + builder.field("description", request.getDescription()); + builder.startArray("targets"); + for (final CreateViewAction.Request.Target target : request.getTargets()) { + builder.startObject(); + builder.field("index_pattern", target.getIndexPattern()); + builder.endObject(); + } + builder.endArray(); + builder.endObject(); + builder.flush(); + source = BytesReference.bytes(builder).utf8ToString(); + } catch (final IOException e) { + throw new OpenSearchException("Failed to parse a request.", e); + } + getCurlRequest(request).body(source).execute(response -> { + try (final XContentParser parser = createParser(response)) { + final GetViewAction.Response getViewResponse = GetViewAction.Response.fromXContent(parser); + listener.onResponse(getViewResponse); + } catch (final Exception e) { + listener.onFailure(toOpenSearchException(response, e)); + } + }, e -> unwrapOpenSearchException(listener, e)); + } + + protected CurlRequest getCurlRequest(final CreateViewAction.Request request) { + // RestViewAction + return client.getCurlRequest(PUT, "/views/" + UrlUtils.encode(request.getName())); + } +} diff --git a/src/main/java/org/codelibs/fesen/client/io/stream/ByteArrayStreamOutput.java b/src/main/java/org/codelibs/fesen/client/io/stream/ByteArrayStreamOutput.java index a4c26ad..3e19d44 100644 --- a/src/main/java/org/codelibs/fesen/client/io/stream/ByteArrayStreamOutput.java +++ b/src/main/java/org/codelibs/fesen/client/io/stream/ByteArrayStreamOutput.java @@ -52,7 +52,7 @@ public void close() throws IOException { @Override public void reset() throws IOException { - throw new UnsupportedOperationException(); + out.reset(); } public byte[] toByteArray() { diff --git a/src/test/java/org/codelibs/fesen/client/action/HttpCreateIndexActionTest.java b/src/test/java/org/codelibs/fesen/client/action/HttpCreateIndexActionTest.java new file mode 100644 index 0000000..fccf5b4 --- /dev/null +++ b/src/test/java/org/codelibs/fesen/client/action/HttpCreateIndexActionTest.java @@ -0,0 +1,113 @@ +/* + * Copyright 2012-2025 CodeLibs Project and the Others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.codelibs.fesen.client.action; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; +import org.opensearch.action.admin.indices.create.CreateIndexAction; +import org.opensearch.action.admin.indices.create.CreateIndexRequest; +import org.opensearch.common.xcontent.json.JsonXContent; +import org.opensearch.core.common.bytes.BytesReference; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.core.xcontent.XContentBuilder; + +class HttpCreateIndexActionTest { + + @Test + void test_innerToXContent_withNoMappingsSet() throws IOException { + final HttpCreateIndexAction action = new HttpCreateIndexAction(null, CreateIndexAction.INSTANCE); + final CreateIndexRequest request = new CreateIndexRequest("test-index"); + // Don't set any mappings + + // Should NOT throw any exception (e.g., UnsupportedOperationException) + final String result = assertDoesNotThrow(() -> { + final XContentBuilder builder = JsonXContent.contentBuilder(); + builder.startObject(); + action.innerToXContent(request, builder, ToXContent.EMPTY_PARAMS); + builder.endObject(); + return BytesReference.bytes(builder).utf8ToString(); + }); + assertTrue(result.contains("settings")); + assertTrue(result.contains("aliases")); + } + + @Test + void test_innerToXContent_withNullMappings() throws IOException { + final HttpCreateIndexAction action = new HttpCreateIndexAction(null, CreateIndexAction.INSTANCE); + // Use a subclass to force mappings() to return null + final CreateIndexRequest request = new CreateIndexRequest("test-index") { + @Override + public String mappings() { + return null; + } + }; + + final XContentBuilder builder = JsonXContent.contentBuilder(); + builder.startObject(); + action.innerToXContent(request, builder, ToXContent.EMPTY_PARAMS); + builder.endObject(); + + final String result = BytesReference.bytes(builder).utf8ToString(); + assertTrue(result.contains("settings")); + assertTrue(result.contains("aliases")); + // With null mappings, no mappings section should appear + assertFalse(result.contains("mappings")); + } + + @Test + void test_innerToXContent_withMappings() throws IOException { + final HttpCreateIndexAction action = new HttpCreateIndexAction(null, CreateIndexAction.INSTANCE); + final CreateIndexRequest request = new CreateIndexRequest("test-index"); + request.mapping("{\"properties\":{\"field1\":{\"type\":\"text\"}}}"); + + final XContentBuilder builder = JsonXContent.contentBuilder(); + builder.startObject(); + action.innerToXContent(request, builder, ToXContent.EMPTY_PARAMS); + builder.endObject(); + + final String result = BytesReference.bytes(builder).utf8ToString(); + assertTrue(result.contains("settings")); + assertTrue(result.contains("aliases")); + assertTrue(result.contains("mappings")); + assertTrue(result.contains("properties")); + assertTrue(result.contains("field1")); + } + + @Test + void test_innerToXContent_withDocWrapperMappings() throws IOException { + final HttpCreateIndexAction action = new HttpCreateIndexAction(null, CreateIndexAction.INSTANCE); + final CreateIndexRequest request = new CreateIndexRequest("test-index"); + request.mapping("{\"_doc\":{\"properties\":{\"field1\":{\"type\":\"keyword\"}}}}"); + + final XContentBuilder builder = JsonXContent.contentBuilder(); + builder.startObject(); + action.innerToXContent(request, builder, ToXContent.EMPTY_PARAMS); + builder.endObject(); + + final String result = BytesReference.bytes(builder).utf8ToString(); + assertTrue(result.contains("mappings")); + assertTrue(result.contains("properties")); + assertTrue(result.contains("field1")); + // The _doc wrapper should be unwrapped + assertFalse(result.contains("_doc")); + } +} diff --git a/src/test/java/org/codelibs/fesen/client/action/HttpCreateViewActionTest.java b/src/test/java/org/codelibs/fesen/client/action/HttpCreateViewActionTest.java new file mode 100644 index 0000000..93a581d --- /dev/null +++ b/src/test/java/org/codelibs/fesen/client/action/HttpCreateViewActionTest.java @@ -0,0 +1,100 @@ +/* + * Copyright 2012-2025 CodeLibs Project and the Others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.codelibs.fesen.client.action; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.opensearch.action.admin.indices.view.CreateViewAction; +import org.opensearch.common.xcontent.json.JsonXContent; +import org.opensearch.core.common.bytes.BytesReference; +import org.opensearch.core.xcontent.XContentBuilder; + +class HttpCreateViewActionTest { + + @Test + void test_construction_withNullClient() { + final HttpCreateViewAction action = new HttpCreateViewAction(null, CreateViewAction.INSTANCE); + assertNotNull(action); + } + + @Test + void test_bodyBuild_withSingleTarget() throws IOException { + final List targets = List.of(new CreateViewAction.Request.Target("logs-*")); + final CreateViewAction.Request request = new CreateViewAction.Request("my-view", "A test view", targets); + + final String source = buildRequestBody(request); + + assertTrue(source.contains("\"name\":\"my-view\"")); + assertTrue(source.contains("\"description\":\"A test view\"")); + assertTrue(source.contains("\"index_pattern\":\"logs-*\"")); + assertTrue(source.contains("\"targets\"")); + } + + @Test + void test_bodyBuild_withMultipleTargets() throws IOException { + final List targets = List.of(new CreateViewAction.Request.Target("logs-*"), + new CreateViewAction.Request.Target("metrics-*"), new CreateViewAction.Request.Target("traces-*")); + final CreateViewAction.Request request = new CreateViewAction.Request("multi-view", "Multi-target view", targets); + + final String source = buildRequestBody(request); + + assertTrue(source.contains("\"name\":\"multi-view\"")); + assertTrue(source.contains("\"description\":\"Multi-target view\"")); + assertTrue(source.contains("\"index_pattern\":\"logs-*\"")); + assertTrue(source.contains("\"index_pattern\":\"metrics-*\"")); + assertTrue(source.contains("\"index_pattern\":\"traces-*\"")); + } + + @Test + void test_bodyBuild_withEmptyDescription() throws IOException { + final List targets = List.of(new CreateViewAction.Request.Target("data-*")); + final CreateViewAction.Request request = new CreateViewAction.Request("no-desc-view", null, targets); + + final String source = buildRequestBody(request); + + assertTrue(source.contains("\"name\":\"no-desc-view\"")); + // null description defaults to empty string in CreateViewAction.Request + assertTrue(source.contains("\"description\":\"\"")); + assertTrue(source.contains("\"index_pattern\":\"data-*\"")); + } + + /** + * Reproduces the body-building logic from HttpCreateViewAction.execute() + * to verify JSON serialization without needing an HTTP connection. + */ + private String buildRequestBody(final CreateViewAction.Request request) throws IOException { + try (final XContentBuilder builder = JsonXContent.contentBuilder()) { + builder.startObject(); + builder.field("name", request.getName()); + builder.field("description", request.getDescription()); + builder.startArray("targets"); + for (final CreateViewAction.Request.Target target : request.getTargets()) { + builder.startObject(); + builder.field("index_pattern", target.getIndexPattern()); + builder.endObject(); + } + builder.endArray(); + builder.endObject(); + builder.flush(); + return BytesReference.bytes(builder).utf8ToString(); + } + } +} diff --git a/src/test/java/org/codelibs/fesen/client/action/HttpDeleteViewActionTest.java b/src/test/java/org/codelibs/fesen/client/action/HttpDeleteViewActionTest.java new file mode 100644 index 0000000..103a7b2 --- /dev/null +++ b/src/test/java/org/codelibs/fesen/client/action/HttpDeleteViewActionTest.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2025 CodeLibs Project and the Others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.codelibs.fesen.client.action; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.Test; +import org.opensearch.action.admin.indices.view.DeleteViewAction; + +class HttpDeleteViewActionTest { + + @Test + void test_construction_withNullClient() { + final HttpDeleteViewAction action = new HttpDeleteViewAction(null, DeleteViewAction.INSTANCE); + assertNotNull(action); + } + + @Test + void test_actionFieldIsSet() { + final HttpDeleteViewAction action = new HttpDeleteViewAction(null, DeleteViewAction.INSTANCE); + assertNotNull(action.action); + } +} diff --git a/src/test/java/org/codelibs/fesen/client/action/HttpGetIngestionStateActionTest.java b/src/test/java/org/codelibs/fesen/client/action/HttpGetIngestionStateActionTest.java new file mode 100644 index 0000000..5247659 --- /dev/null +++ b/src/test/java/org/codelibs/fesen/client/action/HttpGetIngestionStateActionTest.java @@ -0,0 +1,104 @@ +/* + * Copyright 2012-2025 CodeLibs Project and the Others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.codelibs.fesen.client.action; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; +import org.opensearch.action.admin.indices.streamingingestion.state.GetIngestionStateAction; +import org.opensearch.action.admin.indices.streamingingestion.state.GetIngestionStateResponse; +import org.opensearch.common.xcontent.json.JsonXContent; +import org.opensearch.core.xcontent.DeprecationHandler; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.core.xcontent.XContentParser; + +class HttpGetIngestionStateActionTest { + + private final HttpGetIngestionStateAction action = new HttpGetIngestionStateAction(null, GetIngestionStateAction.INSTANCE); + + private XContentParser createParser(final String json) throws IOException { + return JsonXContent.jsonXContent.createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, json); + } + + @Test + void test_fromXContent_basicShards() throws IOException { + final String json = "{\"_shards\": {\"total\": 5, \"successful\": 5, \"failed\": 0}}"; + try (XContentParser parser = createParser(json)) { + final GetIngestionStateResponse response = action.fromXContent(parser); + assertEquals(5, response.getTotalShards()); + assertEquals(5, response.getSuccessfulShards()); + assertEquals(0, response.getFailedShards()); + } + } + + @Test + void test_fromXContent_withFailedShards() throws IOException { + final String json = "{\"_shards\": {\"total\": 10, \"successful\": 8, \"failed\": 2}}"; + try (XContentParser parser = createParser(json)) { + final GetIngestionStateResponse response = action.fromXContent(parser); + assertEquals(10, response.getTotalShards()); + assertEquals(8, response.getSuccessfulShards()); + assertEquals(2, response.getFailedShards()); + } + } + + @Test + void test_fromXContent_withIngestionStateSkipped() throws IOException { + final String json = "{\"_shards\": {\"total\": 10, \"successful\": 8, \"failed\": 2}, " + + "\"ingestion_state\": {\"shard_0\": {\"poller_state\": \"STARTED\"}}}"; + try (XContentParser parser = createParser(json)) { + final GetIngestionStateResponse response = action.fromXContent(parser); + assertEquals(10, response.getTotalShards()); + assertEquals(8, response.getSuccessfulShards()); + assertEquals(2, response.getFailedShards()); + } + } + + @Test + void test_fromXContent_emptyShards() throws IOException { + final String json = "{\"_shards\": {}}"; + try (XContentParser parser = createParser(json)) { + final GetIngestionStateResponse response = action.fromXContent(parser); + assertEquals(0, response.getTotalShards()); + assertEquals(0, response.getSuccessfulShards()); + assertEquals(0, response.getFailedShards()); + } + } + + @Test + void test_fromXContent_emptyResponse() throws IOException { + final String json = "{}"; + try (XContentParser parser = createParser(json)) { + final GetIngestionStateResponse response = action.fromXContent(parser); + assertEquals(0, response.getTotalShards()); + assertEquals(0, response.getSuccessfulShards()); + assertEquals(0, response.getFailedShards()); + } + } + + @Test + void test_fromXContent_invalidToken() { + assertThrows(IOException.class, () -> { + final String json = "[\"not_an_object\"]"; + try (XContentParser parser = createParser(json)) { + action.fromXContent(parser); + } + }); + } +} diff --git a/src/test/java/org/codelibs/fesen/client/action/HttpGetViewActionTest.java b/src/test/java/org/codelibs/fesen/client/action/HttpGetViewActionTest.java new file mode 100644 index 0000000..3d03fb1 --- /dev/null +++ b/src/test/java/org/codelibs/fesen/client/action/HttpGetViewActionTest.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2025 CodeLibs Project and the Others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.codelibs.fesen.client.action; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.Test; +import org.opensearch.action.admin.indices.view.GetViewAction; + +class HttpGetViewActionTest { + + @Test + void test_construction_withNullClient() { + final HttpGetViewAction action = new HttpGetViewAction(null, GetViewAction.INSTANCE); + assertNotNull(action); + } + + @Test + void test_actionFieldIsSet() { + final HttpGetViewAction action = new HttpGetViewAction(null, GetViewAction.INSTANCE); + assertNotNull(action.action); + } +} diff --git a/src/test/java/org/codelibs/fesen/client/action/HttpListViewNamesActionTest.java b/src/test/java/org/codelibs/fesen/client/action/HttpListViewNamesActionTest.java new file mode 100644 index 0000000..76ceb29 --- /dev/null +++ b/src/test/java/org/codelibs/fesen/client/action/HttpListViewNamesActionTest.java @@ -0,0 +1,121 @@ +/* + * Copyright 2012-2025 CodeLibs Project and the Others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.codelibs.fesen.client.action; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.opensearch.action.admin.indices.view.ListViewNamesAction; +import org.opensearch.common.xcontent.LoggingDeprecationHandler; +import org.opensearch.common.xcontent.json.JsonXContent; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.core.xcontent.XContentParser; + +class HttpListViewNamesActionTest { + + @Test + void test_construction_withNullClient() { + final HttpListViewNamesAction action = new HttpListViewNamesAction(null, ListViewNamesAction.INSTANCE); + assertNotNull(action); + } + + @Test + void test_parseViewNames_multipleViews() throws IOException { + final String json = """ + {"views": ["view1", "view2", "view3"]}"""; + + final List viewNames = parseViewNames(json); + + assertEquals(3, viewNames.size()); + assertEquals("view1", viewNames.get(0)); + assertEquals("view2", viewNames.get(1)); + assertEquals("view3", viewNames.get(2)); + } + + @Test + void test_parseViewNames_emptyViews() throws IOException { + final String json = """ + {"views": []}"""; + + final List viewNames = parseViewNames(json); + + assertNotNull(viewNames); + assertTrue(viewNames.isEmpty()); + } + + @Test + void test_parseViewNames_singleView() throws IOException { + final String json = """ + {"views": ["only-view"]}"""; + + final List viewNames = parseViewNames(json); + + assertEquals(1, viewNames.size()); + assertEquals("only-view", viewNames.get(0)); + } + + @Test + void test_parseViewNames_viewNamesWithSpecialCharacters() throws IOException { + final String json = """ + {"views": ["my-view-1", "view_with_underscores", "view.with.dots"]}"""; + + final List viewNames = parseViewNames(json); + + assertEquals(3, viewNames.size()); + assertEquals("my-view-1", viewNames.get(0)); + assertEquals("view_with_underscores", viewNames.get(1)); + assertEquals("view.with.dots", viewNames.get(2)); + } + + @Test + void test_parseViewNames_manyViews() throws IOException { + final String json = """ + {"views": ["v1", "v2", "v3", "v4", "v5", "v6", "v7", "v8", "v9", "v10"]}"""; + + final List viewNames = parseViewNames(json); + + assertEquals(10, viewNames.size()); + assertEquals("v1", viewNames.get(0)); + assertEquals("v10", viewNames.get(9)); + } + + /** + * Reproduces the parsing logic from HttpListViewNamesAction.execute() + * to verify the custom fromXContent parser without needing an HTTP connection. + */ + private List parseViewNames(final String json) throws IOException { + try (final XContentParser parser = + JsonXContent.jsonXContent.createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, json)) { + final List viewNames = new ArrayList<>(); + XContentParser.Token token = parser.nextToken(); + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME && "views".equals(parser.currentName())) { + parser.nextToken(); // START_ARRAY + while (parser.nextToken() != XContentParser.Token.END_ARRAY) { + viewNames.add(parser.text()); + } + } + } + return viewNames; + } + } +} diff --git a/src/test/java/org/codelibs/fesen/client/action/HttpPauseIngestionActionTest.java b/src/test/java/org/codelibs/fesen/client/action/HttpPauseIngestionActionTest.java new file mode 100644 index 0000000..1fa6628 --- /dev/null +++ b/src/test/java/org/codelibs/fesen/client/action/HttpPauseIngestionActionTest.java @@ -0,0 +1,91 @@ +/* + * Copyright 2012-2025 CodeLibs Project and the Others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.codelibs.fesen.client.action; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; +import org.opensearch.action.admin.indices.streamingingestion.pause.PauseIngestionAction; +import org.opensearch.action.admin.indices.streamingingestion.pause.PauseIngestionResponse; +import org.opensearch.common.xcontent.json.JsonXContent; +import org.opensearch.core.xcontent.DeprecationHandler; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.core.xcontent.XContentParser; + +class HttpPauseIngestionActionTest { + + private final HttpPauseIngestionAction action = new HttpPauseIngestionAction(null, PauseIngestionAction.INSTANCE); + + private XContentParser createParser(final String json) throws IOException { + return JsonXContent.jsonXContent.createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, json); + } + + @Test + void test_fromXContent_allTrue() throws IOException { + final String json = "{\"acknowledged\": true, \"shards_acknowledged\": true}"; + try (XContentParser parser = createParser(json)) { + final PauseIngestionResponse response = action.fromXContent(parser); + assertTrue(response.isAcknowledged()); + assertTrue(response.isShardsAcknowledged()); + } + } + + @Test + void test_fromXContent_acknowledgedTrueShardsAcknowledgedFalse() throws IOException { + final String json = "{\"acknowledged\": true, \"shards_acknowledged\": false}"; + try (XContentParser parser = createParser(json)) { + final PauseIngestionResponse response = action.fromXContent(parser); + assertTrue(response.isAcknowledged()); + assertFalse(response.isShardsAcknowledged()); + } + } + + @Test + void test_fromXContent_allFalse() throws IOException { + final String json = "{\"acknowledged\": false, \"shards_acknowledged\": false}"; + try (XContentParser parser = createParser(json)) { + final PauseIngestionResponse response = action.fromXContent(parser); + assertFalse(response.isAcknowledged()); + assertFalse(response.isShardsAcknowledged()); + } + } + + @Test + void test_fromXContent_withExtraFields() throws IOException { + final String json = "{\"acknowledged\": true, \"shards_acknowledged\": true, " + + "\"error\": {\"type\": \"some_error\", \"reason\": \"test\"}, " + "\"failures\": [{\"shard\": 0}]}"; + try (XContentParser parser = createParser(json)) { + final PauseIngestionResponse response = action.fromXContent(parser); + assertTrue(response.isAcknowledged()); + assertTrue(response.isShardsAcknowledged()); + } + } + + @Test + void test_fromXContent_invalidToken() { + assertThrows(IOException.class, () -> { + final String json = "[\"not_an_object\"]"; + try (XContentParser parser = createParser(json)) { + action.fromXContent(parser); + } + }); + } +} diff --git a/src/test/java/org/codelibs/fesen/client/action/HttpRemoteStoreMetadataActionTest.java b/src/test/java/org/codelibs/fesen/client/action/HttpRemoteStoreMetadataActionTest.java new file mode 100644 index 0000000..6fb7090 --- /dev/null +++ b/src/test/java/org/codelibs/fesen/client/action/HttpRemoteStoreMetadataActionTest.java @@ -0,0 +1,104 @@ +/* + * Copyright 2012-2025 CodeLibs Project and the Others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.codelibs.fesen.client.action; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; +import org.opensearch.action.admin.cluster.remotestore.metadata.RemoteStoreMetadataAction; +import org.opensearch.action.admin.cluster.remotestore.metadata.RemoteStoreMetadataResponse; +import org.opensearch.common.xcontent.json.JsonXContent; +import org.opensearch.core.xcontent.DeprecationHandler; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.core.xcontent.XContentParser; + +class HttpRemoteStoreMetadataActionTest { + + private final HttpRemoteStoreMetadataAction action = new HttpRemoteStoreMetadataAction(null, RemoteStoreMetadataAction.INSTANCE); + + private XContentParser createParser(final String json) throws IOException { + return JsonXContent.jsonXContent.createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, json); + } + + @Test + void test_fromXContent_basicShards() throws IOException { + final String json = "{\"_shards\": {\"total\": 5, \"successful\": 5, \"failed\": 0}}"; + try (XContentParser parser = createParser(json)) { + final RemoteStoreMetadataResponse response = action.fromXContent(parser); + assertEquals(5, response.getTotalShards()); + assertEquals(5, response.getSuccessfulShards()); + assertEquals(0, response.getFailedShards()); + } + } + + @Test + void test_fromXContent_withFailedShards() throws IOException { + final String json = "{\"_shards\": {\"total\": 10, \"successful\": 8, \"failed\": 2}}"; + try (XContentParser parser = createParser(json)) { + final RemoteStoreMetadataResponse response = action.fromXContent(parser); + assertEquals(10, response.getTotalShards()); + assertEquals(8, response.getSuccessfulShards()); + assertEquals(2, response.getFailedShards()); + } + } + + @Test + void test_fromXContent_withIndicesSkipped() throws IOException { + final String json = "{\"_shards\": {\"total\": 3, \"successful\": 3, \"failed\": 0}, " + + "\"indices\": {\"my_index\": {\"shards\": {\"0\": {\"segment\": {}}}}}}"; + try (XContentParser parser = createParser(json)) { + final RemoteStoreMetadataResponse response = action.fromXContent(parser); + assertEquals(3, response.getTotalShards()); + assertEquals(3, response.getSuccessfulShards()); + assertEquals(0, response.getFailedShards()); + } + } + + @Test + void test_fromXContent_emptyShards() throws IOException { + final String json = "{\"_shards\": {}}"; + try (XContentParser parser = createParser(json)) { + final RemoteStoreMetadataResponse response = action.fromXContent(parser); + assertEquals(0, response.getTotalShards()); + assertEquals(0, response.getSuccessfulShards()); + assertEquals(0, response.getFailedShards()); + } + } + + @Test + void test_fromXContent_emptyResponse() throws IOException { + final String json = "{}"; + try (XContentParser parser = createParser(json)) { + final RemoteStoreMetadataResponse response = action.fromXContent(parser); + assertEquals(0, response.getTotalShards()); + assertEquals(0, response.getSuccessfulShards()); + assertEquals(0, response.getFailedShards()); + } + } + + @Test + void test_fromXContent_invalidToken() { + assertThrows(IOException.class, () -> { + final String json = "[\"not_an_object\"]"; + try (XContentParser parser = createParser(json)) { + action.fromXContent(parser); + } + }); + } +} diff --git a/src/test/java/org/codelibs/fesen/client/action/HttpResumeIngestionActionTest.java b/src/test/java/org/codelibs/fesen/client/action/HttpResumeIngestionActionTest.java new file mode 100644 index 0000000..bbacf65 --- /dev/null +++ b/src/test/java/org/codelibs/fesen/client/action/HttpResumeIngestionActionTest.java @@ -0,0 +1,90 @@ +/* + * Copyright 2012-2025 CodeLibs Project and the Others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.codelibs.fesen.client.action; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; +import org.opensearch.action.admin.indices.streamingingestion.resume.ResumeIngestionAction; +import org.opensearch.action.admin.indices.streamingingestion.resume.ResumeIngestionResponse; +import org.opensearch.common.xcontent.json.JsonXContent; +import org.opensearch.core.xcontent.DeprecationHandler; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.core.xcontent.XContentParser; + +class HttpResumeIngestionActionTest { + + private final HttpResumeIngestionAction action = new HttpResumeIngestionAction(null, ResumeIngestionAction.INSTANCE); + + private XContentParser createParser(final String json) throws IOException { + return JsonXContent.jsonXContent.createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, json); + } + + @Test + void test_fromXContent_allTrue() throws IOException { + final String json = "{\"acknowledged\": true, \"shards_acknowledged\": true}"; + try (XContentParser parser = createParser(json)) { + final ResumeIngestionResponse response = action.fromXContent(parser); + assertTrue(response.isAcknowledged()); + assertTrue(response.isShardsAcknowledged()); + } + } + + @Test + void test_fromXContent_acknowledgedTrueShardsAcknowledgedFalse() throws IOException { + final String json = "{\"acknowledged\": true, \"shards_acknowledged\": false}"; + try (XContentParser parser = createParser(json)) { + final ResumeIngestionResponse response = action.fromXContent(parser); + assertTrue(response.isAcknowledged()); + assertFalse(response.isShardsAcknowledged()); + } + } + + @Test + void test_fromXContent_allFalse() throws IOException { + final String json = "{\"acknowledged\": false, \"shards_acknowledged\": false}"; + try (XContentParser parser = createParser(json)) { + final ResumeIngestionResponse response = action.fromXContent(parser); + assertFalse(response.isAcknowledged()); + assertFalse(response.isShardsAcknowledged()); + } + } + + @Test + void test_fromXContent_withExtraFields() throws IOException { + final String json = "{\"acknowledged\": true, \"shards_acknowledged\": true, " + + "\"error\": {\"type\": \"some_error\", \"reason\": \"test\"}, " + "\"failures\": [{\"shard\": 0}]}"; + try (XContentParser parser = createParser(json)) { + final ResumeIngestionResponse response = action.fromXContent(parser); + assertTrue(response.isAcknowledged()); + assertTrue(response.isShardsAcknowledged()); + } + } + + @Test + void test_fromXContent_invalidToken() { + assertThrows(IOException.class, () -> { + final String json = "[\"not_an_object\"]"; + try (XContentParser parser = createParser(json)) { + action.fromXContent(parser); + } + }); + } +} diff --git a/src/test/java/org/codelibs/fesen/client/action/HttpScaleIndexActionTest.java b/src/test/java/org/codelibs/fesen/client/action/HttpScaleIndexActionTest.java new file mode 100644 index 0000000..836a6b9 --- /dev/null +++ b/src/test/java/org/codelibs/fesen/client/action/HttpScaleIndexActionTest.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-2025 CodeLibs Project and the Others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.codelibs.fesen.client.action; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.Test; +import org.opensearch.action.admin.indices.scale.searchonly.ScaleIndexAction; + +class HttpScaleIndexActionTest { + + @Test + void test_construction() { + final HttpScaleIndexAction action = new HttpScaleIndexAction(null, ScaleIndexAction.INSTANCE); + assertNotNull(action); + } +} diff --git a/src/test/java/org/codelibs/fesen/client/action/HttpSearchViewActionTest.java b/src/test/java/org/codelibs/fesen/client/action/HttpSearchViewActionTest.java new file mode 100644 index 0000000..9869127 --- /dev/null +++ b/src/test/java/org/codelibs/fesen/client/action/HttpSearchViewActionTest.java @@ -0,0 +1,85 @@ +/* + * Copyright 2012-2025 CodeLibs Project and the Others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.codelibs.fesen.client.action; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import org.opensearch.action.admin.indices.view.SearchViewAction; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.index.query.QueryBuilders; +import org.opensearch.search.builder.SearchSourceBuilder; + +class HttpSearchViewActionTest { + + @Test + void test_construction_withNullClient() { + final HttpSearchViewAction action = new HttpSearchViewAction(null, SearchViewAction.INSTANCE); + assertNotNull(action); + } + + @Test + void test_getQuerySource_withNullSource() { + final HttpSearchViewAction action = new HttpSearchViewAction(null, SearchViewAction.INSTANCE); + final SearchRequest searchRequest = new SearchRequest(); + // Do not set source - it should be null + final SearchViewAction.Request request = new SearchViewAction.Request("my-view", searchRequest); + + final String querySource = action.getQuerySource(request); + assertNull(querySource); + } + + @Test + void test_getQuerySource_withMatchAllQuery() { + final HttpSearchViewAction action = new HttpSearchViewAction(null, SearchViewAction.INSTANCE); + final SearchRequest searchRequest = new SearchRequest(); + searchRequest.source(new SearchSourceBuilder().query(QueryBuilders.matchAllQuery())); + final SearchViewAction.Request request = new SearchViewAction.Request("my-view", searchRequest); + + final String querySource = action.getQuerySource(request); + assertNotNull(querySource); + assertTrue(querySource.contains("match_all")); + } + + @Test + void test_getQuerySource_withTermQuery() { + final HttpSearchViewAction action = new HttpSearchViewAction(null, SearchViewAction.INSTANCE); + final SearchRequest searchRequest = new SearchRequest(); + searchRequest.source(new SearchSourceBuilder().query(QueryBuilders.termQuery("status", "active"))); + final SearchViewAction.Request request = new SearchViewAction.Request("my-view", searchRequest); + + final String querySource = action.getQuerySource(request); + assertNotNull(querySource); + assertTrue(querySource.contains("term")); + assertTrue(querySource.contains("status")); + assertTrue(querySource.contains("active")); + } + + @Test + void test_getQuerySource_withSizeAndFrom() { + final HttpSearchViewAction action = new HttpSearchViewAction(null, SearchViewAction.INSTANCE); + final SearchRequest searchRequest = new SearchRequest(); + searchRequest.source(new SearchSourceBuilder().size(10).from(20)); + final SearchViewAction.Request request = new SearchViewAction.Request("my-view", searchRequest); + + final String querySource = action.getQuerySource(request); + assertNotNull(querySource); + assertTrue(querySource.contains("\"size\":10")); + assertTrue(querySource.contains("\"from\":20")); + } +} diff --git a/src/test/java/org/codelibs/fesen/client/action/HttpUpdateViewActionTest.java b/src/test/java/org/codelibs/fesen/client/action/HttpUpdateViewActionTest.java new file mode 100644 index 0000000..b2bd197 --- /dev/null +++ b/src/test/java/org/codelibs/fesen/client/action/HttpUpdateViewActionTest.java @@ -0,0 +1,97 @@ +/* + * Copyright 2012-2025 CodeLibs Project and the Others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.codelibs.fesen.client.action; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.opensearch.action.admin.indices.view.CreateViewAction; +import org.opensearch.action.admin.indices.view.UpdateViewAction; +import org.opensearch.common.xcontent.json.JsonXContent; +import org.opensearch.core.common.bytes.BytesReference; +import org.opensearch.core.xcontent.XContentBuilder; + +class HttpUpdateViewActionTest { + + @Test + void test_construction_withNullClient() { + final HttpUpdateViewAction action = new HttpUpdateViewAction(null, UpdateViewAction.INSTANCE); + assertNotNull(action); + } + + @Test + void test_bodyBuild_doesNotContainName() throws IOException { + final List targets = List.of(new CreateViewAction.Request.Target("logs-*")); + final CreateViewAction.Request request = new CreateViewAction.Request("my-view", "Updated description", targets); + + final String source = buildUpdateBody(request); + + // Update body should NOT contain name (name is in the URL path) + assertFalse(source.contains("\"name\"")); + assertTrue(source.contains("\"description\":\"Updated description\"")); + assertTrue(source.contains("\"index_pattern\":\"logs-*\"")); + } + + @Test + void test_bodyBuild_withMultipleTargets() throws IOException { + final List targets = + List.of(new CreateViewAction.Request.Target("logs-*"), new CreateViewAction.Request.Target("events-*")); + final CreateViewAction.Request request = new CreateViewAction.Request("my-view", "Multi-target update", targets); + + final String source = buildUpdateBody(request); + + assertTrue(source.contains("\"description\":\"Multi-target update\"")); + assertTrue(source.contains("\"index_pattern\":\"logs-*\"")); + assertTrue(source.contains("\"index_pattern\":\"events-*\"")); + } + + @Test + void test_bodyBuild_withEmptyDescription() throws IOException { + final List targets = List.of(new CreateViewAction.Request.Target("data-*")); + final CreateViewAction.Request request = new CreateViewAction.Request("my-view", null, targets); + + final String source = buildUpdateBody(request); + + // null description defaults to empty string + assertTrue(source.contains("\"description\":\"\"")); + } + + /** + * Reproduces the body-building logic from HttpUpdateViewAction.execute() + * (no name field, unlike create). + */ + private String buildUpdateBody(final CreateViewAction.Request request) throws IOException { + try (final XContentBuilder builder = JsonXContent.contentBuilder()) { + builder.startObject(); + builder.field("description", request.getDescription()); + builder.startArray("targets"); + for (final CreateViewAction.Request.Target target : request.getTargets()) { + builder.startObject(); + builder.field("index_pattern", target.getIndexPattern()); + builder.endObject(); + } + builder.endArray(); + builder.endObject(); + builder.flush(); + return BytesReference.bytes(builder).utf8ToString(); + } + } +} diff --git a/src/test/java/org/codelibs/fesen/client/io/stream/ByteArrayStreamOutputTest.java b/src/test/java/org/codelibs/fesen/client/io/stream/ByteArrayStreamOutputTest.java new file mode 100644 index 0000000..7996005 --- /dev/null +++ b/src/test/java/org/codelibs/fesen/client/io/stream/ByteArrayStreamOutputTest.java @@ -0,0 +1,116 @@ +/* + * Copyright 2012-2025 CodeLibs Project and the Others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.codelibs.fesen.client.io.stream; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; +import org.opensearch.core.common.io.stream.StreamInput; + +class ByteArrayStreamOutputTest { + + @Test + void test_reset() throws IOException { + final ByteArrayStreamOutput output = new ByteArrayStreamOutput(); + output.writeByte((byte) 1); + output.writeByte((byte) 2); + assertEquals(2, output.toByteArray().length); + + output.reset(); + assertEquals(0, output.toByteArray().length); + + // Write after reset + output.writeByte((byte) 3); + assertEquals(1, output.toByteArray().length); + assertEquals(3, output.toByteArray()[0]); + } + + @Test + void test_writeByte() throws IOException { + final ByteArrayStreamOutput output = new ByteArrayStreamOutput(); + output.writeByte((byte) 42); + final byte[] result = output.toByteArray(); + assertEquals(1, result.length); + assertEquals(42, result[0]); + } + + @Test + void test_writeBytes() throws IOException { + final ByteArrayStreamOutput output = new ByteArrayStreamOutput(); + final byte[] data = { 10, 20, 30, 40, 50 }; + output.writeBytes(data, 1, 3); + final byte[] result = output.toByteArray(); + assertEquals(3, result.length); + assertArrayEquals(new byte[] { 20, 30, 40 }, result); + } + + @Test + void test_flushAndClose() throws IOException { + final ByteArrayStreamOutput output = new ByteArrayStreamOutput(); + output.writeByte((byte) 1); + output.flush(); + assertEquals(1, output.toByteArray().length); + output.close(); + } + + @Test + void test_toByteArray() throws IOException { + final ByteArrayStreamOutput output = new ByteArrayStreamOutput(); + output.writeByte((byte) 0xA); + output.writeByte((byte) 0xB); + output.writeByte((byte) 0xC); + assertArrayEquals(new byte[] { 0xA, 0xB, 0xC }, output.toByteArray()); + } + + @Test + void test_toStreamInput() throws IOException { + final ByteArrayStreamOutput output = new ByteArrayStreamOutput(); + output.writeByte((byte) 100); + output.writeByte((byte) 101); + final StreamInput input = output.toStreamInput(); + assertNotNull(input); + assertEquals(100, input.readByte()); + assertEquals(101, input.readByte()); + } + + @Test + void test_multipleResetCycles() throws IOException { + final ByteArrayStreamOutput output = new ByteArrayStreamOutput(); + + // First cycle + output.writeByte((byte) 1); + output.writeByte((byte) 2); + assertEquals(2, output.toByteArray().length); + + // Reset and second cycle + output.reset(); + assertEquals(0, output.toByteArray().length); + output.writeByte((byte) 10); + assertEquals(1, output.toByteArray().length); + assertEquals(10, output.toByteArray()[0]); + + // Reset and third cycle + output.reset(); + assertEquals(0, output.toByteArray().length); + output.writeBytes(new byte[] { 20, 30, 40 }, 0, 3); + assertEquals(3, output.toByteArray().length); + assertArrayEquals(new byte[] { 20, 30, 40 }, output.toByteArray()); + } +} From 44a857d3d5b2888ff81e85453f1333df884261bf Mon Sep 17 00:00:00 2001 From: Shinsuke Sugaya Date: Sat, 21 Mar 2026 18:19:38 +0900 Subject: [PATCH 2/2] fix: address Codex review findings for response parsing and test quality - HttpGetIngestionStateAction: fully parse ingestion_state per-shard data and next_page_token instead of returning empty arrays - HttpRemoteStoreMetadataAction: fully parse indices/shards metadata instead of skipping the indices section - HttpListViewNamesAction: remove trailing slash from /views/ endpoint - HttpScaleIndexAction: add setAccessible for package-private class access and document why reflection is required - HttpCreateViewAction/HttpUpdateViewAction: extract buildRequestBody() for testability - HttpListViewNamesAction: extract parseViewNames() for testability - Update all tests to exercise production code instead of duplicated helpers, add payload parsing and URL encoding coverage --- .../client/action/HttpCreateViewAction.java | 23 ++--- .../action/HttpGetIngestionStateAction.java | 75 +++++++++++++++- .../action/HttpListViewNamesAction.java | 28 +++--- .../action/HttpRemoteStoreMetadataAction.java | 87 ++++++++++++++++++- .../client/action/HttpScaleIndexAction.java | 5 +- .../client/action/HttpUpdateViewAction.java | 23 ++--- .../action/HttpCreateViewActionTest.java | 41 ++------- .../action/HttpDeleteViewActionTest.java | 24 +++++ .../HttpGetIngestionStateActionTest.java | 84 ++++++++++++++++-- .../client/action/HttpGetViewActionTest.java | 24 +++++ .../action/HttpListViewNamesActionTest.java | 36 +++----- .../HttpRemoteStoreMetadataActionTest.java | 68 +++++++++++++-- .../action/HttpScaleIndexActionTest.java | 60 +++++++++++++ .../action/HttpUpdateViewActionTest.java | 40 ++------- 14 files changed, 478 insertions(+), 140 deletions(-) diff --git a/src/main/java/org/codelibs/fesen/client/action/HttpCreateViewAction.java b/src/main/java/org/codelibs/fesen/client/action/HttpCreateViewAction.java index 52bad86..1cc5835 100644 --- a/src/main/java/org/codelibs/fesen/client/action/HttpCreateViewAction.java +++ b/src/main/java/org/codelibs/fesen/client/action/HttpCreateViewAction.java @@ -38,7 +38,18 @@ public HttpCreateViewAction(final HttpClient client, final CreateViewAction acti } public void execute(final CreateViewAction.Request request, final ActionListener listener) { - String source = null; + final String source = buildRequestBody(request); + getCurlRequest(request).body(source).execute(response -> { + try (final XContentParser parser = createParser(response)) { + final GetViewAction.Response getViewResponse = GetViewAction.Response.fromXContent(parser); + listener.onResponse(getViewResponse); + } catch (final Exception e) { + listener.onFailure(toOpenSearchException(response, e)); + } + }, e -> unwrapOpenSearchException(listener, e)); + } + + protected String buildRequestBody(final CreateViewAction.Request request) { try (final XContentBuilder builder = JsonXContent.contentBuilder()) { builder.startObject(); builder.field("name", request.getName()); @@ -52,18 +63,10 @@ public void execute(final CreateViewAction.Request request, final ActionListener builder.endArray(); builder.endObject(); builder.flush(); - source = BytesReference.bytes(builder).utf8ToString(); + return BytesReference.bytes(builder).utf8ToString(); } catch (final IOException e) { throw new OpenSearchException("Failed to parse a request.", e); } - getCurlRequest(request).body(source).execute(response -> { - try (final XContentParser parser = createParser(response)) { - final GetViewAction.Response getViewResponse = GetViewAction.Response.fromXContent(parser); - listener.onResponse(getViewResponse); - } catch (final Exception e) { - listener.onFailure(toOpenSearchException(response, e)); - } - }, e -> unwrapOpenSearchException(listener, e)); } protected CurlRequest getCurlRequest(final CreateViewAction.Request request) { diff --git a/src/main/java/org/codelibs/fesen/client/action/HttpGetIngestionStateAction.java b/src/main/java/org/codelibs/fesen/client/action/HttpGetIngestionStateAction.java index 853ca1d..69bd6c5 100644 --- a/src/main/java/org/codelibs/fesen/client/action/HttpGetIngestionStateAction.java +++ b/src/main/java/org/codelibs/fesen/client/action/HttpGetIngestionStateAction.java @@ -56,7 +56,9 @@ protected GetIngestionStateResponse fromXContent(final XContentParser parser) th int totalShards = 0; int successfulShards = 0; int failedShards = 0; + String nextPageToken = null; final List shardFailures = new ArrayList<>(); + final List shardStates = new ArrayList<>(); XContentParser.Token token = parser.nextToken(); if (token != XContentParser.Token.START_OBJECT) { @@ -66,6 +68,10 @@ protected GetIngestionStateResponse fromXContent(final XContentParser parser) th while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { if (token == XContentParser.Token.FIELD_NAME) { fieldName = parser.currentName(); + } else if (token == XContentParser.Token.VALUE_STRING) { + if ("next_page_token".equals(fieldName)) { + nextPageToken = parser.text(); + } } else if (token == XContentParser.Token.START_OBJECT) { if ("_shards".equals(fieldName)) { while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { @@ -81,6 +87,8 @@ protected GetIngestionStateResponse fromXContent(final XContentParser parser) th } } } + } else if ("ingestion_state".equals(fieldName)) { + parseIngestionState(parser, shardStates); } else { consumeObject(parser); } @@ -89,7 +97,72 @@ protected GetIngestionStateResponse fromXContent(final XContentParser parser) th } } - return new GetIngestionStateResponse(new ShardIngestionState[0], totalShards, successfulShards, failedShards, null, shardFailures); + return new GetIngestionStateResponse(shardStates.toArray(new ShardIngestionState[0]), totalShards, successfulShards, failedShards, + nextPageToken, shardFailures); + } + + protected void parseIngestionState(final XContentParser parser, final List shardStates) throws IOException { + // ingestion_state: { "index_name": [ {shard fields...}, ... ], ... } + XContentParser.Token token; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + final String indexName = parser.currentName(); + token = parser.nextToken(); // START_ARRAY + if (token == XContentParser.Token.START_ARRAY) { + while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { + if (token == XContentParser.Token.START_OBJECT) { + shardStates.add(parseShardIngestionState(parser, indexName)); + } + } + } + } + } + } + + protected ShardIngestionState parseShardIngestionState(final XContentParser parser, final String indexName) throws IOException { + int shardId = -1; + String pollerState = null; + String errorPolicy = null; + boolean pollerPaused = false; + boolean writeBlockEnabled = false; + String batchStartPointer = ""; + boolean isPrimary = true; + String nodeName = ""; + + XContentParser.Token token; + String fieldName = null; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + fieldName = parser.currentName(); + } else if (token == XContentParser.Token.VALUE_NUMBER) { + if ("shard".equals(fieldName)) { + shardId = parser.intValue(); + } + } else if (token == XContentParser.Token.VALUE_STRING) { + if ("poller_state".equals(fieldName)) { + pollerState = parser.text(); + } else if ("error_policy".equals(fieldName)) { + errorPolicy = parser.text(); + } else if ("batch_start_pointer".equals(fieldName)) { + batchStartPointer = parser.text(); + } else if ("node".equals(fieldName)) { + nodeName = parser.text(); + } + } else if (token == XContentParser.Token.VALUE_BOOLEAN) { + if ("poller_paused".equals(fieldName)) { + pollerPaused = parser.booleanValue(); + } else if ("write_block_enabled".equals(fieldName)) { + writeBlockEnabled = parser.booleanValue(); + } else if ("is_primary".equals(fieldName)) { + isPrimary = parser.booleanValue(); + } + } else if (token == XContentParser.Token.START_OBJECT || token == XContentParser.Token.START_ARRAY) { + consumeObject(parser); + } + } + + return new ShardIngestionState(indexName, shardId, pollerState, errorPolicy, pollerPaused, writeBlockEnabled, batchStartPointer, + isPrimary, nodeName); } protected void consumeObject(final XContentParser parser) throws IOException { diff --git a/src/main/java/org/codelibs/fesen/client/action/HttpListViewNamesAction.java b/src/main/java/org/codelibs/fesen/client/action/HttpListViewNamesAction.java index 99468ce..193945e 100644 --- a/src/main/java/org/codelibs/fesen/client/action/HttpListViewNamesAction.java +++ b/src/main/java/org/codelibs/fesen/client/action/HttpListViewNamesAction.java @@ -15,6 +15,7 @@ */ package org.codelibs.fesen.client.action; +import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -36,16 +37,7 @@ public HttpListViewNamesAction(final HttpClient client, final ListViewNamesActio public void execute(final ListViewNamesAction.Request request, final ActionListener listener) { getCurlRequest(request).execute(response -> { try (final XContentParser parser = createParser(response)) { - final List viewNames = new ArrayList<>(); - XContentParser.Token token = parser.nextToken(); - while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { - if (token == XContentParser.Token.FIELD_NAME && "views".equals(parser.currentName())) { - parser.nextToken(); // START_ARRAY - while (parser.nextToken() != XContentParser.Token.END_ARRAY) { - viewNames.add(parser.text()); - } - } - } + final List viewNames = parseViewNames(parser); listener.onResponse(new ListViewNamesAction.Response(viewNames)); } catch (final Exception e) { listener.onFailure(toOpenSearchException(response, e)); @@ -53,8 +45,22 @@ public void execute(final ListViewNamesAction.Request request, final ActionListe }, e -> unwrapOpenSearchException(listener, e)); } + protected List parseViewNames(final XContentParser parser) throws IOException { + final List viewNames = new ArrayList<>(); + XContentParser.Token token = parser.nextToken(); + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME && "views".equals(parser.currentName())) { + parser.nextToken(); // START_ARRAY + while (parser.nextToken() != XContentParser.Token.END_ARRAY) { + viewNames.add(parser.text()); + } + } + } + return viewNames; + } + protected CurlRequest getCurlRequest(final ListViewNamesAction.Request request) { // RestViewAction - return client.getCurlRequest(GET, "/views/"); + return client.getCurlRequest(GET, "/views"); } } diff --git a/src/main/java/org/codelibs/fesen/client/action/HttpRemoteStoreMetadataAction.java b/src/main/java/org/codelibs/fesen/client/action/HttpRemoteStoreMetadataAction.java index e19fef2..c8227f4 100644 --- a/src/main/java/org/codelibs/fesen/client/action/HttpRemoteStoreMetadataAction.java +++ b/src/main/java/org/codelibs/fesen/client/action/HttpRemoteStoreMetadataAction.java @@ -17,7 +17,9 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.codelibs.curl.CurlRequest; import org.codelibs.fesen.client.HttpClient; @@ -56,6 +58,7 @@ protected RemoteStoreMetadataResponse fromXContent(final XContentParser parser) int successfulShards = 0; int failedShards = 0; final List shardFailures = new ArrayList<>(); + final List metadataList = new ArrayList<>(); // Initialize parser - move to START_OBJECT XContentParser.Token token = parser.nextToken(); @@ -83,16 +86,94 @@ protected RemoteStoreMetadataResponse fromXContent(final XContentParser parser) } } } else if ("indices".equals(fieldName)) { - // Skip indices section - complex to parse + parseIndices(parser, metadataList); + } else { consumeObject(parser); + } + } + } + + return new RemoteStoreMetadataResponse(metadataList.toArray(new RemoteStoreShardMetadata[0]), totalShards, successfulShards, + failedShards, shardFailures); + } + + @SuppressWarnings("unchecked") + protected void parseIndices(final XContentParser parser, final List metadataList) throws IOException { + // indices: { "index_name": { "shards": { "0": [ {shard metadata...} ] } } } + XContentParser.Token token; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + final String indexName = parser.currentName(); + parser.nextToken(); // START_OBJECT for index + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME && "shards".equals(parser.currentName())) { + parser.nextToken(); // START_OBJECT for shards + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + final int shardId = Integer.parseInt(parser.currentName()); + parser.nextToken(); // START_ARRAY + while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { + if (token == XContentParser.Token.START_OBJECT) { + metadataList.add(parseShardMetadata(parser, indexName, shardId)); + } + } + } + } + } else if (token == XContentParser.Token.START_OBJECT || token == XContentParser.Token.START_ARRAY) { + consumeObject(parser); + } + } + } + } + } + + @SuppressWarnings("unchecked") + protected RemoteStoreShardMetadata parseShardMetadata(final XContentParser parser, final String indexName, final int shardId) + throws IOException { + String latestSegmentMetadataFileName = null; + String latestTranslogMetadataFileName = null; + Map> segmentMetadataFiles = new HashMap<>(); + Map> translogMetadataFiles = new HashMap<>(); + + XContentParser.Token token; + String fieldName = null; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + fieldName = parser.currentName(); + } else if (token == XContentParser.Token.VALUE_STRING) { + if ("latest_segment_metadata_filename".equals(fieldName)) { + latestSegmentMetadataFileName = parser.text(); + } else if ("latest_translog_metadata_filename".equals(fieldName)) { + latestTranslogMetadataFileName = parser.text(); + } + } else if (token == XContentParser.Token.START_OBJECT) { + if ("available_segment_metadata_files".equals(fieldName)) { + segmentMetadataFiles = parseMetadataFiles(parser); + } else if ("available_translog_metadata_files".equals(fieldName)) { + translogMetadataFiles = parseMetadataFiles(parser); } else { consumeObject(parser); } + } else if (token == XContentParser.Token.START_ARRAY) { + consumeObject(parser); } } - // Build RemoteStoreMetadataResponse with empty RemoteStoreShardMetadata array - return new RemoteStoreMetadataResponse(new RemoteStoreShardMetadata[0], totalShards, successfulShards, failedShards, shardFailures); + return new RemoteStoreShardMetadata(indexName, shardId, segmentMetadataFiles, translogMetadataFiles, latestSegmentMetadataFileName, + latestTranslogMetadataFileName); + } + + protected Map> parseMetadataFiles(final XContentParser parser) throws IOException { + final Map> files = new HashMap<>(); + XContentParser.Token token; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + final String fileName = parser.currentName(); + parser.nextToken(); // START_OBJECT + files.put(fileName, parser.map()); + } + } + return files; } protected void consumeObject(final XContentParser parser) throws IOException { diff --git a/src/main/java/org/codelibs/fesen/client/action/HttpScaleIndexAction.java b/src/main/java/org/codelibs/fesen/client/action/HttpScaleIndexAction.java index 57c4a57..7b2f444 100644 --- a/src/main/java/org/codelibs/fesen/client/action/HttpScaleIndexAction.java +++ b/src/main/java/org/codelibs/fesen/client/action/HttpScaleIndexAction.java @@ -36,10 +36,13 @@ public HttpScaleIndexAction(final HttpClient client, final ScaleIndexAction acti } public void execute(final ActionRequest request, final ActionListener listener) { - // Use reflection to access package-private ScaleIndexRequest fields + // ScaleIndexRequest is package-private in OpenSearch, so reflection is the only way + // to access getIndex() and isScaleDown() from outside the package. try { final Method getIndexMethod = request.getClass().getMethod("getIndex"); + getIndexMethod.setAccessible(true); final Method isScaleDownMethod = request.getClass().getMethod("isScaleDown"); + isScaleDownMethod.setAccessible(true); final String index = (String) getIndexMethod.invoke(request); final boolean scaleDown = (boolean) isScaleDownMethod.invoke(request); diff --git a/src/main/java/org/codelibs/fesen/client/action/HttpUpdateViewAction.java b/src/main/java/org/codelibs/fesen/client/action/HttpUpdateViewAction.java index 45d5095..8f6272a 100644 --- a/src/main/java/org/codelibs/fesen/client/action/HttpUpdateViewAction.java +++ b/src/main/java/org/codelibs/fesen/client/action/HttpUpdateViewAction.java @@ -40,7 +40,18 @@ public HttpUpdateViewAction(final HttpClient client, final UpdateViewAction acti } public void execute(final CreateViewAction.Request request, final ActionListener listener) { - String source = null; + final String source = buildRequestBody(request); + getCurlRequest(request).body(source).execute(response -> { + try (final XContentParser parser = createParser(response)) { + final GetViewAction.Response getViewResponse = GetViewAction.Response.fromXContent(parser); + listener.onResponse(getViewResponse); + } catch (final Exception e) { + listener.onFailure(toOpenSearchException(response, e)); + } + }, e -> unwrapOpenSearchException(listener, e)); + } + + protected String buildRequestBody(final CreateViewAction.Request request) { try (final XContentBuilder builder = JsonXContent.contentBuilder()) { builder.startObject(); builder.field("description", request.getDescription()); @@ -53,18 +64,10 @@ public void execute(final CreateViewAction.Request request, final ActionListener builder.endArray(); builder.endObject(); builder.flush(); - source = BytesReference.bytes(builder).utf8ToString(); + return BytesReference.bytes(builder).utf8ToString(); } catch (final IOException e) { throw new OpenSearchException("Failed to parse a request.", e); } - getCurlRequest(request).body(source).execute(response -> { - try (final XContentParser parser = createParser(response)) { - final GetViewAction.Response getViewResponse = GetViewAction.Response.fromXContent(parser); - listener.onResponse(getViewResponse); - } catch (final Exception e) { - listener.onFailure(toOpenSearchException(response, e)); - } - }, e -> unwrapOpenSearchException(listener, e)); } protected CurlRequest getCurlRequest(final CreateViewAction.Request request) { diff --git a/src/test/java/org/codelibs/fesen/client/action/HttpCreateViewActionTest.java b/src/test/java/org/codelibs/fesen/client/action/HttpCreateViewActionTest.java index 93a581d..886a542 100644 --- a/src/test/java/org/codelibs/fesen/client/action/HttpCreateViewActionTest.java +++ b/src/test/java/org/codelibs/fesen/client/action/HttpCreateViewActionTest.java @@ -18,29 +18,26 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.io.IOException; import java.util.List; import org.junit.jupiter.api.Test; import org.opensearch.action.admin.indices.view.CreateViewAction; -import org.opensearch.common.xcontent.json.JsonXContent; -import org.opensearch.core.common.bytes.BytesReference; -import org.opensearch.core.xcontent.XContentBuilder; class HttpCreateViewActionTest { + private final HttpCreateViewAction action = new HttpCreateViewAction(null, CreateViewAction.INSTANCE); + @Test void test_construction_withNullClient() { - final HttpCreateViewAction action = new HttpCreateViewAction(null, CreateViewAction.INSTANCE); assertNotNull(action); } @Test - void test_bodyBuild_withSingleTarget() throws IOException { + void test_buildRequestBody_withSingleTarget() { final List targets = List.of(new CreateViewAction.Request.Target("logs-*")); final CreateViewAction.Request request = new CreateViewAction.Request("my-view", "A test view", targets); - final String source = buildRequestBody(request); + final String source = action.buildRequestBody(request); assertTrue(source.contains("\"name\":\"my-view\"")); assertTrue(source.contains("\"description\":\"A test view\"")); @@ -49,12 +46,12 @@ void test_bodyBuild_withSingleTarget() throws IOException { } @Test - void test_bodyBuild_withMultipleTargets() throws IOException { + void test_buildRequestBody_withMultipleTargets() { final List targets = List.of(new CreateViewAction.Request.Target("logs-*"), new CreateViewAction.Request.Target("metrics-*"), new CreateViewAction.Request.Target("traces-*")); final CreateViewAction.Request request = new CreateViewAction.Request("multi-view", "Multi-target view", targets); - final String source = buildRequestBody(request); + final String source = action.buildRequestBody(request); assertTrue(source.contains("\"name\":\"multi-view\"")); assertTrue(source.contains("\"description\":\"Multi-target view\"")); @@ -64,37 +61,15 @@ void test_bodyBuild_withMultipleTargets() throws IOException { } @Test - void test_bodyBuild_withEmptyDescription() throws IOException { + void test_buildRequestBody_withEmptyDescription() { final List targets = List.of(new CreateViewAction.Request.Target("data-*")); final CreateViewAction.Request request = new CreateViewAction.Request("no-desc-view", null, targets); - final String source = buildRequestBody(request); + final String source = action.buildRequestBody(request); assertTrue(source.contains("\"name\":\"no-desc-view\"")); // null description defaults to empty string in CreateViewAction.Request assertTrue(source.contains("\"description\":\"\"")); assertTrue(source.contains("\"index_pattern\":\"data-*\"")); } - - /** - * Reproduces the body-building logic from HttpCreateViewAction.execute() - * to verify JSON serialization without needing an HTTP connection. - */ - private String buildRequestBody(final CreateViewAction.Request request) throws IOException { - try (final XContentBuilder builder = JsonXContent.contentBuilder()) { - builder.startObject(); - builder.field("name", request.getName()); - builder.field("description", request.getDescription()); - builder.startArray("targets"); - for (final CreateViewAction.Request.Target target : request.getTargets()) { - builder.startObject(); - builder.field("index_pattern", target.getIndexPattern()); - builder.endObject(); - } - builder.endArray(); - builder.endObject(); - builder.flush(); - return BytesReference.bytes(builder).utf8ToString(); - } - } } diff --git a/src/test/java/org/codelibs/fesen/client/action/HttpDeleteViewActionTest.java b/src/test/java/org/codelibs/fesen/client/action/HttpDeleteViewActionTest.java index 103a7b2..01bb7fe 100644 --- a/src/test/java/org/codelibs/fesen/client/action/HttpDeleteViewActionTest.java +++ b/src/test/java/org/codelibs/fesen/client/action/HttpDeleteViewActionTest.java @@ -15,8 +15,10 @@ */ package org.codelibs.fesen.client.action; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import org.codelibs.fesen.client.util.UrlUtils; import org.junit.jupiter.api.Test; import org.opensearch.action.admin.indices.view.DeleteViewAction; @@ -33,4 +35,26 @@ void test_actionFieldIsSet() { final HttpDeleteViewAction action = new HttpDeleteViewAction(null, DeleteViewAction.INSTANCE); assertNotNull(action.action); } + + @Test + void test_urlPath_simpleViewName() { + // Verify the URL path pattern used by getCurlRequest: /views/{encoded_name} + final String viewName = "my-test-view"; + final String path = "/views/" + UrlUtils.encode(viewName); + assertEquals("/views/my-test-view", path); + } + + @Test + void test_urlPath_viewNameWithSpecialCharacters() { + final String viewName = "view with spaces"; + final String path = "/views/" + UrlUtils.encode(viewName); + assertEquals("/views/view+with+spaces", path); + } + + @Test + void test_urlPath_viewNameWithSlash() { + final String viewName = "ns/view"; + final String path = "/views/" + UrlUtils.encode(viewName); + assertEquals("/views/ns%2Fview", path); + } } diff --git a/src/test/java/org/codelibs/fesen/client/action/HttpGetIngestionStateActionTest.java b/src/test/java/org/codelibs/fesen/client/action/HttpGetIngestionStateActionTest.java index 5247659..18ea98f 100644 --- a/src/test/java/org/codelibs/fesen/client/action/HttpGetIngestionStateActionTest.java +++ b/src/test/java/org/codelibs/fesen/client/action/HttpGetIngestionStateActionTest.java @@ -16,13 +16,18 @@ package org.codelibs.fesen.client.action; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; import org.junit.jupiter.api.Test; import org.opensearch.action.admin.indices.streamingingestion.state.GetIngestionStateAction; import org.opensearch.action.admin.indices.streamingingestion.state.GetIngestionStateResponse; +import org.opensearch.action.admin.indices.streamingingestion.state.ShardIngestionState; import org.opensearch.common.xcontent.json.JsonXContent; import org.opensearch.core.xcontent.DeprecationHandler; import org.opensearch.core.xcontent.NamedXContentRegistry; @@ -44,6 +49,7 @@ void test_fromXContent_basicShards() throws IOException { assertEquals(5, response.getTotalShards()); assertEquals(5, response.getSuccessfulShards()); assertEquals(0, response.getFailedShards()); + assertEquals(0, response.getShardStates().length); } } @@ -59,14 +65,76 @@ void test_fromXContent_withFailedShards() throws IOException { } @Test - void test_fromXContent_withIngestionStateSkipped() throws IOException { - final String json = "{\"_shards\": {\"total\": 10, \"successful\": 8, \"failed\": 2}, " - + "\"ingestion_state\": {\"shard_0\": {\"poller_state\": \"STARTED\"}}}"; + void test_fromXContent_withIngestionStateParsed() throws IOException { + final String json = "{\"_shards\": {\"total\": 2, \"successful\": 2, \"failed\": 0}, " + "\"ingestion_state\": {\"my-index\": [" + + "{\"shard\": 0, \"poller_state\": \"STARTED\", \"error_policy\": \"DROP\", " + + "\"poller_paused\": false, \"write_block_enabled\": false, " + + "\"batch_start_pointer\": \"100\", \"is_primary\": true, \"node\": \"node1\"}," + + "{\"shard\": 1, \"poller_state\": \"PAUSED\", \"error_policy\": \"BLOCK\", " + + "\"poller_paused\": true, \"write_block_enabled\": true, " + + "\"batch_start_pointer\": \"200\", \"is_primary\": false, \"node\": \"node2\"}" + "]}}"; try (XContentParser parser = createParser(json)) { final GetIngestionStateResponse response = action.fromXContent(parser); - assertEquals(10, response.getTotalShards()); - assertEquals(8, response.getSuccessfulShards()); - assertEquals(2, response.getFailedShards()); + assertEquals(2, response.getTotalShards()); + assertEquals(2, response.getSuccessfulShards()); + assertEquals(0, response.getFailedShards()); + + final ShardIngestionState[] states = response.getShardStates(); + assertNotNull(states); + assertEquals(2, states.length); + + assertEquals("my-index", states[0].getIndex()); + assertEquals(0, states[0].getShardId()); + assertEquals("STARTED", states[0].getPollerState()); + assertEquals("DROP", states[0].getErrorPolicy()); + assertFalse(states[0].isPollerPaused()); + assertFalse(states[0].isWriteBlockEnabled()); + assertEquals("100", states[0].getBatchStartPointer()); + assertTrue(states[0].isPrimary()); + assertEquals("node1", states[0].getNodeName()); + + assertEquals("my-index", states[1].getIndex()); + assertEquals(1, states[1].getShardId()); + assertEquals("PAUSED", states[1].getPollerState()); + assertEquals("BLOCK", states[1].getErrorPolicy()); + assertTrue(states[1].isPollerPaused()); + assertTrue(states[1].isWriteBlockEnabled()); + assertEquals("200", states[1].getBatchStartPointer()); + assertFalse(states[1].isPrimary()); + assertEquals("node2", states[1].getNodeName()); + } + } + + @Test + void test_fromXContent_withMultipleIndices() throws IOException { + final String json = "{\"_shards\": {\"total\": 2, \"successful\": 2, \"failed\": 0}, " + "\"ingestion_state\": {" + + "\"index-a\": [{\"shard\": 0, \"poller_state\": \"STARTED\", \"error_policy\": \"DROP\", " + + "\"poller_paused\": false, \"write_block_enabled\": false, \"batch_start_pointer\": \"0\", " + + "\"is_primary\": true, \"node\": \"n1\"}]," + + "\"index-b\": [{\"shard\": 0, \"poller_state\": \"PAUSED\", \"error_policy\": \"BLOCK\", " + + "\"poller_paused\": true, \"write_block_enabled\": false, \"batch_start_pointer\": \"50\", " + + "\"is_primary\": true, \"node\": \"n2\"}]" + "}}"; + try (XContentParser parser = createParser(json)) { + final GetIngestionStateResponse response = action.fromXContent(parser); + final ShardIngestionState[] states = response.getShardStates(); + assertEquals(2, states.length); + + // States should contain entries from both indices + assertTrue(states[0].getIndex().equals("index-a") || states[0].getIndex().equals("index-b")); + assertTrue(states[1].getIndex().equals("index-a") || states[1].getIndex().equals("index-b")); + } + } + + @Test + void test_fromXContent_withNextPageToken() throws IOException { + final String json = "{\"_shards\": {\"total\": 1, \"successful\": 1, \"failed\": 0}, " + "\"next_page_token\": \"abc123\", " + + "\"ingestion_state\": {\"my-index\": [" + "{\"shard\": 0, \"poller_state\": \"STARTED\", \"error_policy\": \"DROP\", " + + "\"poller_paused\": false, \"write_block_enabled\": false, " + + "\"batch_start_pointer\": \"0\", \"is_primary\": true, \"node\": \"node1\"}" + "]}}"; + try (XContentParser parser = createParser(json)) { + final GetIngestionStateResponse response = action.fromXContent(parser); + assertEquals("abc123", response.getNextPageToken()); + assertEquals(1, response.getShardStates().length); } } @@ -78,6 +146,7 @@ void test_fromXContent_emptyShards() throws IOException { assertEquals(0, response.getTotalShards()); assertEquals(0, response.getSuccessfulShards()); assertEquals(0, response.getFailedShards()); + assertNull(response.getNextPageToken()); } } @@ -87,8 +156,7 @@ void test_fromXContent_emptyResponse() throws IOException { try (XContentParser parser = createParser(json)) { final GetIngestionStateResponse response = action.fromXContent(parser); assertEquals(0, response.getTotalShards()); - assertEquals(0, response.getSuccessfulShards()); - assertEquals(0, response.getFailedShards()); + assertEquals(0, response.getShardStates().length); } } diff --git a/src/test/java/org/codelibs/fesen/client/action/HttpGetViewActionTest.java b/src/test/java/org/codelibs/fesen/client/action/HttpGetViewActionTest.java index 3d03fb1..f002016 100644 --- a/src/test/java/org/codelibs/fesen/client/action/HttpGetViewActionTest.java +++ b/src/test/java/org/codelibs/fesen/client/action/HttpGetViewActionTest.java @@ -15,8 +15,10 @@ */ package org.codelibs.fesen.client.action; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import org.codelibs.fesen.client.util.UrlUtils; import org.junit.jupiter.api.Test; import org.opensearch.action.admin.indices.view.GetViewAction; @@ -33,4 +35,26 @@ void test_actionFieldIsSet() { final HttpGetViewAction action = new HttpGetViewAction(null, GetViewAction.INSTANCE); assertNotNull(action.action); } + + @Test + void test_urlPath_simpleViewName() { + // Verify the URL path pattern used by getCurlRequest: /views/{encoded_name} + final String viewName = "my-test-view"; + final String path = "/views/" + UrlUtils.encode(viewName); + assertEquals("/views/my-test-view", path); + } + + @Test + void test_urlPath_viewNameWithSpecialCharacters() { + final String viewName = "view with spaces"; + final String path = "/views/" + UrlUtils.encode(viewName); + assertEquals("/views/view+with+spaces", path); + } + + @Test + void test_urlPath_viewNameWithSlash() { + final String viewName = "ns/view"; + final String path = "/views/" + UrlUtils.encode(viewName); + assertEquals("/views/ns%2Fview", path); + } } diff --git a/src/test/java/org/codelibs/fesen/client/action/HttpListViewNamesActionTest.java b/src/test/java/org/codelibs/fesen/client/action/HttpListViewNamesActionTest.java index 76ceb29..c80e95c 100644 --- a/src/test/java/org/codelibs/fesen/client/action/HttpListViewNamesActionTest.java +++ b/src/test/java/org/codelibs/fesen/client/action/HttpListViewNamesActionTest.java @@ -20,21 +20,21 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; -import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.Test; import org.opensearch.action.admin.indices.view.ListViewNamesAction; -import org.opensearch.common.xcontent.LoggingDeprecationHandler; import org.opensearch.common.xcontent.json.JsonXContent; +import org.opensearch.core.xcontent.DeprecationHandler; import org.opensearch.core.xcontent.NamedXContentRegistry; import org.opensearch.core.xcontent.XContentParser; class HttpListViewNamesActionTest { + private final HttpListViewNamesAction action = new HttpListViewNamesAction(null, ListViewNamesAction.INSTANCE); + @Test void test_construction_withNullClient() { - final HttpListViewNamesAction action = new HttpListViewNamesAction(null, ListViewNamesAction.INSTANCE); assertNotNull(action); } @@ -43,7 +43,7 @@ void test_parseViewNames_multipleViews() throws IOException { final String json = """ {"views": ["view1", "view2", "view3"]}"""; - final List viewNames = parseViewNames(json); + final List viewNames = parseWithProductionCode(json); assertEquals(3, viewNames.size()); assertEquals("view1", viewNames.get(0)); @@ -56,7 +56,7 @@ void test_parseViewNames_emptyViews() throws IOException { final String json = """ {"views": []}"""; - final List viewNames = parseViewNames(json); + final List viewNames = parseWithProductionCode(json); assertNotNull(viewNames); assertTrue(viewNames.isEmpty()); @@ -67,7 +67,7 @@ void test_parseViewNames_singleView() throws IOException { final String json = """ {"views": ["only-view"]}"""; - final List viewNames = parseViewNames(json); + final List viewNames = parseWithProductionCode(json); assertEquals(1, viewNames.size()); assertEquals("only-view", viewNames.get(0)); @@ -78,7 +78,7 @@ void test_parseViewNames_viewNamesWithSpecialCharacters() throws IOException { final String json = """ {"views": ["my-view-1", "view_with_underscores", "view.with.dots"]}"""; - final List viewNames = parseViewNames(json); + final List viewNames = parseWithProductionCode(json); assertEquals(3, viewNames.size()); assertEquals("my-view-1", viewNames.get(0)); @@ -91,31 +91,17 @@ void test_parseViewNames_manyViews() throws IOException { final String json = """ {"views": ["v1", "v2", "v3", "v4", "v5", "v6", "v7", "v8", "v9", "v10"]}"""; - final List viewNames = parseViewNames(json); + final List viewNames = parseWithProductionCode(json); assertEquals(10, viewNames.size()); assertEquals("v1", viewNames.get(0)); assertEquals("v10", viewNames.get(9)); } - /** - * Reproduces the parsing logic from HttpListViewNamesAction.execute() - * to verify the custom fromXContent parser without needing an HTTP connection. - */ - private List parseViewNames(final String json) throws IOException { + private List parseWithProductionCode(final String json) throws IOException { try (final XContentParser parser = - JsonXContent.jsonXContent.createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, json)) { - final List viewNames = new ArrayList<>(); - XContentParser.Token token = parser.nextToken(); - while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { - if (token == XContentParser.Token.FIELD_NAME && "views".equals(parser.currentName())) { - parser.nextToken(); // START_ARRAY - while (parser.nextToken() != XContentParser.Token.END_ARRAY) { - viewNames.add(parser.text()); - } - } - } - return viewNames; + JsonXContent.jsonXContent.createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, json)) { + return action.parseViewNames(parser); } } } diff --git a/src/test/java/org/codelibs/fesen/client/action/HttpRemoteStoreMetadataActionTest.java b/src/test/java/org/codelibs/fesen/client/action/HttpRemoteStoreMetadataActionTest.java index 6fb7090..a339650 100644 --- a/src/test/java/org/codelibs/fesen/client/action/HttpRemoteStoreMetadataActionTest.java +++ b/src/test/java/org/codelibs/fesen/client/action/HttpRemoteStoreMetadataActionTest.java @@ -16,13 +16,19 @@ package org.codelibs.fesen.client.action; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; +import java.util.List; +import java.util.Map; import org.junit.jupiter.api.Test; import org.opensearch.action.admin.cluster.remotestore.metadata.RemoteStoreMetadataAction; import org.opensearch.action.admin.cluster.remotestore.metadata.RemoteStoreMetadataResponse; +import org.opensearch.action.admin.cluster.remotestore.metadata.RemoteStoreShardMetadata; import org.opensearch.common.xcontent.json.JsonXContent; import org.opensearch.core.xcontent.DeprecationHandler; import org.opensearch.core.xcontent.NamedXContentRegistry; @@ -59,14 +65,64 @@ void test_fromXContent_withFailedShards() throws IOException { } @Test - void test_fromXContent_withIndicesSkipped() throws IOException { - final String json = "{\"_shards\": {\"total\": 3, \"successful\": 3, \"failed\": 0}, " - + "\"indices\": {\"my_index\": {\"shards\": {\"0\": {\"segment\": {}}}}}}"; + void test_fromXContent_withIndicesParsed() throws IOException { + final String json = + "{\"_shards\": {\"total\": 1, \"successful\": 1, \"failed\": 0}, " + "\"indices\": {\"my_index\": {\"shards\": {\"0\": [{" + + "\"index\": \"my_index\", \"shard\": 0, " + "\"latest_segment_metadata_filename\": \"seg_meta_001\", " + + "\"latest_translog_metadata_filename\": \"translog_meta_001\", " + + "\"available_segment_metadata_files\": {\"file1\": {\"size\": 1024}}, " + + "\"available_translog_metadata_files\": {\"tfile1\": {\"size\": 512}}" + "}]}}}}"; try (XContentParser parser = createParser(json)) { final RemoteStoreMetadataResponse response = action.fromXContent(parser); - assertEquals(3, response.getTotalShards()); - assertEquals(3, response.getSuccessfulShards()); - assertEquals(0, response.getFailedShards()); + assertEquals(1, response.getTotalShards()); + + final Map>> grouped = response.groupByIndexAndShards(); + assertNotNull(grouped); + assertTrue(grouped.containsKey("my_index")); + + final List shardMetadata = grouped.get("my_index").get(0); + assertNotNull(shardMetadata); + assertEquals(1, shardMetadata.size()); + + final RemoteStoreShardMetadata metadata = shardMetadata.get(0); + assertEquals("my_index", metadata.getIndexName()); + assertEquals(0, metadata.getShardId()); + assertEquals("seg_meta_001", metadata.getLatestSegmentMetadataFileName()); + assertEquals("translog_meta_001", metadata.getLatestTranslogMetadataFileName()); + assertNotNull(metadata.getSegmentMetadataFiles()); + assertTrue(metadata.getSegmentMetadataFiles().containsKey("file1")); + assertNotNull(metadata.getTranslogMetadataFiles()); + assertTrue(metadata.getTranslogMetadataFiles().containsKey("tfile1")); + } + } + + @Test + void test_fromXContent_withMultipleIndicesAndShards() throws IOException { + final String json = "{\"_shards\": {\"total\": 2, \"successful\": 2, \"failed\": 0}, " + "\"indices\": {" + + "\"index_a\": {\"shards\": {" + "\"0\": [{\"index\": \"index_a\", \"shard\": 0, " + + "\"available_segment_metadata_files\": {}, \"available_translog_metadata_files\": {}}]" + "}}," + + "\"index_b\": {\"shards\": {" + "\"0\": [{\"index\": \"index_b\", \"shard\": 0, " + + "\"available_segment_metadata_files\": {}, \"available_translog_metadata_files\": {}}]" + "}}" + "}}"; + try (XContentParser parser = createParser(json)) { + final RemoteStoreMetadataResponse response = action.fromXContent(parser); + final Map>> grouped = response.groupByIndexAndShards(); + assertEquals(2, grouped.size()); + assertTrue(grouped.containsKey("index_a")); + assertTrue(grouped.containsKey("index_b")); + } + } + + @Test + void test_fromXContent_withoutMetadataFileNames() throws IOException { + final String json = "{\"_shards\": {\"total\": 1, \"successful\": 1, \"failed\": 0}, " + + "\"indices\": {\"my_index\": {\"shards\": {\"0\": [{" + "\"index\": \"my_index\", \"shard\": 0, " + + "\"available_segment_metadata_files\": {}, " + "\"available_translog_metadata_files\": {}" + "}]}}}}"; + try (XContentParser parser = createParser(json)) { + final RemoteStoreMetadataResponse response = action.fromXContent(parser); + final Map>> grouped = response.groupByIndexAndShards(); + final RemoteStoreShardMetadata metadata = grouped.get("my_index").get(0).get(0); + assertNull(metadata.getLatestSegmentMetadataFileName()); + assertNull(metadata.getLatestTranslogMetadataFileName()); } } diff --git a/src/test/java/org/codelibs/fesen/client/action/HttpScaleIndexActionTest.java b/src/test/java/org/codelibs/fesen/client/action/HttpScaleIndexActionTest.java index 836a6b9..6eff0da 100644 --- a/src/test/java/org/codelibs/fesen/client/action/HttpScaleIndexActionTest.java +++ b/src/test/java/org/codelibs/fesen/client/action/HttpScaleIndexActionTest.java @@ -15,10 +15,16 @@ */ package org.codelibs.fesen.client.action; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.Method; import org.junit.jupiter.api.Test; import org.opensearch.action.admin.indices.scale.searchonly.ScaleIndexAction; +import org.opensearch.action.admin.indices.scale.searchonly.ScaleIndexRequestBuilder; class HttpScaleIndexActionTest { @@ -27,4 +33,58 @@ void test_construction() { final HttpScaleIndexAction action = new HttpScaleIndexAction(null, ScaleIndexAction.INSTANCE); assertNotNull(action); } + + @Test + void test_scaleIndexRequest_hasExpectedReflectionMethods_scaleDown() throws Exception { + // Verify that ScaleIndexRequest (package-private) has the methods accessed via reflection + final ScaleIndexRequestBuilder builder = new ScaleIndexRequestBuilder(null, true, "test-index"); + final Object request = builder.request(); + + final Method getIndexMethod = request.getClass().getMethod("getIndex"); + getIndexMethod.setAccessible(true); + final Method isScaleDownMethod = request.getClass().getMethod("isScaleDown"); + isScaleDownMethod.setAccessible(true); + + assertEquals("test-index", getIndexMethod.invoke(request)); + assertTrue((boolean) isScaleDownMethod.invoke(request)); + } + + @Test + void test_scaleIndexRequest_hasExpectedReflectionMethods_scaleUp() throws Exception { + final ScaleIndexRequestBuilder builder = new ScaleIndexRequestBuilder(null, false, "my-index"); + final Object request = builder.request(); + + final Method getIndexMethod = request.getClass().getMethod("getIndex"); + getIndexMethod.setAccessible(true); + final Method isScaleDownMethod = request.getClass().getMethod("isScaleDown"); + isScaleDownMethod.setAccessible(true); + + assertEquals("my-index", getIndexMethod.invoke(request)); + assertFalse((boolean) isScaleDownMethod.invoke(request)); + } + + @Test + void test_requestBodyFormat_scaleDown() throws Exception { + // Verify the JSON body format that HttpScaleIndexAction.execute() would produce + final ScaleIndexRequestBuilder builder = new ScaleIndexRequestBuilder(null, true, "test-index"); + final Object request = builder.request(); + + final Method isScaleDownMethod = request.getClass().getMethod("isScaleDown"); + isScaleDownMethod.setAccessible(true); + final boolean scaleDown = (boolean) isScaleDownMethod.invoke(request); + final String body = "{\"search_only\":" + scaleDown + "}"; + assertEquals("{\"search_only\":true}", body); + } + + @Test + void test_requestBodyFormat_scaleUp() throws Exception { + final ScaleIndexRequestBuilder builder = new ScaleIndexRequestBuilder(null, false, "test-index"); + final Object request = builder.request(); + + final Method isScaleDownMethod = request.getClass().getMethod("isScaleDown"); + isScaleDownMethod.setAccessible(true); + final boolean scaleDown = (boolean) isScaleDownMethod.invoke(request); + final String body = "{\"search_only\":" + scaleDown + "}"; + assertEquals("{\"search_only\":false}", body); + } } diff --git a/src/test/java/org/codelibs/fesen/client/action/HttpUpdateViewActionTest.java b/src/test/java/org/codelibs/fesen/client/action/HttpUpdateViewActionTest.java index b2bd197..f4d3afe 100644 --- a/src/test/java/org/codelibs/fesen/client/action/HttpUpdateViewActionTest.java +++ b/src/test/java/org/codelibs/fesen/client/action/HttpUpdateViewActionTest.java @@ -19,30 +19,27 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.io.IOException; import java.util.List; import org.junit.jupiter.api.Test; import org.opensearch.action.admin.indices.view.CreateViewAction; import org.opensearch.action.admin.indices.view.UpdateViewAction; -import org.opensearch.common.xcontent.json.JsonXContent; -import org.opensearch.core.common.bytes.BytesReference; -import org.opensearch.core.xcontent.XContentBuilder; class HttpUpdateViewActionTest { + private final HttpUpdateViewAction action = new HttpUpdateViewAction(null, UpdateViewAction.INSTANCE); + @Test void test_construction_withNullClient() { - final HttpUpdateViewAction action = new HttpUpdateViewAction(null, UpdateViewAction.INSTANCE); assertNotNull(action); } @Test - void test_bodyBuild_doesNotContainName() throws IOException { + void test_buildRequestBody_doesNotContainName() { final List targets = List.of(new CreateViewAction.Request.Target("logs-*")); final CreateViewAction.Request request = new CreateViewAction.Request("my-view", "Updated description", targets); - final String source = buildUpdateBody(request); + final String source = action.buildRequestBody(request); // Update body should NOT contain name (name is in the URL path) assertFalse(source.contains("\"name\"")); @@ -51,12 +48,12 @@ void test_bodyBuild_doesNotContainName() throws IOException { } @Test - void test_bodyBuild_withMultipleTargets() throws IOException { + void test_buildRequestBody_withMultipleTargets() { final List targets = List.of(new CreateViewAction.Request.Target("logs-*"), new CreateViewAction.Request.Target("events-*")); final CreateViewAction.Request request = new CreateViewAction.Request("my-view", "Multi-target update", targets); - final String source = buildUpdateBody(request); + final String source = action.buildRequestBody(request); assertTrue(source.contains("\"description\":\"Multi-target update\"")); assertTrue(source.contains("\"index_pattern\":\"logs-*\"")); @@ -64,34 +61,13 @@ void test_bodyBuild_withMultipleTargets() throws IOException { } @Test - void test_bodyBuild_withEmptyDescription() throws IOException { + void test_buildRequestBody_withEmptyDescription() { final List targets = List.of(new CreateViewAction.Request.Target("data-*")); final CreateViewAction.Request request = new CreateViewAction.Request("my-view", null, targets); - final String source = buildUpdateBody(request); + final String source = action.buildRequestBody(request); // null description defaults to empty string assertTrue(source.contains("\"description\":\"\"")); } - - /** - * Reproduces the body-building logic from HttpUpdateViewAction.execute() - * (no name field, unlike create). - */ - private String buildUpdateBody(final CreateViewAction.Request request) throws IOException { - try (final XContentBuilder builder = JsonXContent.contentBuilder()) { - builder.startObject(); - builder.field("description", request.getDescription()); - builder.startArray("targets"); - for (final CreateViewAction.Request.Target target : request.getTargets()) { - builder.startObject(); - builder.field("index_pattern", target.getIndexPattern()); - builder.endObject(); - } - builder.endArray(); - builder.endObject(); - builder.flush(); - return BytesReference.bytes(builder).utf8ToString(); - } - } }