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..1cc5835 --- /dev/null +++ b/src/main/java/org/codelibs/fesen/client/action/HttpCreateViewAction.java @@ -0,0 +1,76 @@ +/* + * 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) { + 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()); + 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(); + } catch (final IOException e) { + throw new OpenSearchException("Failed to parse a request.", 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..69bd6c5 --- /dev/null +++ b/src/main/java/org/codelibs/fesen/client/action/HttpGetIngestionStateAction.java @@ -0,0 +1,192 @@ +/* + * 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; + String nextPageToken = null; + final List shardFailures = new ArrayList<>(); + final List shardStates = 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.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) { + 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 ("ingestion_state".equals(fieldName)) { + parseIngestionState(parser, shardStates); + } else { + consumeObject(parser); + } + } else if (token == XContentParser.Token.START_ARRAY) { + consumeObject(parser); + } + } + + 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 { + 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..193945e --- /dev/null +++ b/src/main/java/org/codelibs/fesen/client/action/HttpListViewNamesAction.java @@ -0,0 +1,66 @@ +/* + * 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.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 = parseViewNames(parser); + listener.onResponse(new ListViewNamesAction.Response(viewNames)); + } catch (final Exception e) { + listener.onFailure(toOpenSearchException(response, e)); + } + }, 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"); + } +} 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..c8227f4 --- /dev/null +++ b/src/main/java/org/codelibs/fesen/client/action/HttpRemoteStoreMetadataAction.java @@ -0,0 +1,204 @@ +/* + * 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.HashMap; +import java.util.List; +import java.util.Map; + +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<>(); + final List metadataList = 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)) { + 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); + } + } + + 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 { + 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..7b2f444 --- /dev/null +++ b/src/main/java/org/codelibs/fesen/client/action/HttpScaleIndexAction.java @@ -0,0 +1,63 @@ +/* + * 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) { + // 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); + + 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..8f6272a --- /dev/null +++ b/src/main/java/org/codelibs/fesen/client/action/HttpUpdateViewAction.java @@ -0,0 +1,77 @@ +/* + * 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) { + 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()); + 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(); + } catch (final IOException e) { + throw new OpenSearchException("Failed to parse a request.", 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..886a542 --- /dev/null +++ b/src/test/java/org/codelibs/fesen/client/action/HttpCreateViewActionTest.java @@ -0,0 +1,75 @@ +/* + * 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.util.List; + +import org.junit.jupiter.api.Test; +import org.opensearch.action.admin.indices.view.CreateViewAction; + +class HttpCreateViewActionTest { + + private final HttpCreateViewAction action = new HttpCreateViewAction(null, CreateViewAction.INSTANCE); + + @Test + void test_construction_withNullClient() { + assertNotNull(action); + } + + @Test + 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 = action.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_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 = action.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_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 = 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-*\"")); + } +} 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..01bb7fe --- /dev/null +++ b/src/test/java/org/codelibs/fesen/client/action/HttpDeleteViewActionTest.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 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; + +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); + } + + @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 new file mode 100644 index 0000000..18ea98f --- /dev/null +++ b/src/test/java/org/codelibs/fesen/client/action/HttpGetIngestionStateActionTest.java @@ -0,0 +1,172 @@ +/* + * 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.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; +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()); + assertEquals(0, response.getShardStates().length); + } + } + + @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_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(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); + } + } + + @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()); + assertNull(response.getNextPageToken()); + } + } + + @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.getShardStates().length); + } + } + + @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..f002016 --- /dev/null +++ b/src/test/java/org/codelibs/fesen/client/action/HttpGetViewActionTest.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 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; + +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); + } + + @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 new file mode 100644 index 0000000..c80e95c --- /dev/null +++ b/src/test/java/org/codelibs/fesen/client/action/HttpListViewNamesActionTest.java @@ -0,0 +1,107 @@ +/* + * 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.List; + +import org.junit.jupiter.api.Test; +import org.opensearch.action.admin.indices.view.ListViewNamesAction; +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() { + assertNotNull(action); + } + + @Test + void test_parseViewNames_multipleViews() throws IOException { + final String json = """ + {"views": ["view1", "view2", "view3"]}"""; + + final List viewNames = parseWithProductionCode(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 = parseWithProductionCode(json); + + assertNotNull(viewNames); + assertTrue(viewNames.isEmpty()); + } + + @Test + void test_parseViewNames_singleView() throws IOException { + final String json = """ + {"views": ["only-view"]}"""; + + final List viewNames = parseWithProductionCode(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 = parseWithProductionCode(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 = parseWithProductionCode(json); + + assertEquals(10, viewNames.size()); + assertEquals("v1", viewNames.get(0)); + assertEquals("v10", viewNames.get(9)); + } + + private List parseWithProductionCode(final String json) throws IOException { + try (final XContentParser parser = + 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/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..a339650 --- /dev/null +++ b/src/test/java/org/codelibs/fesen/client/action/HttpRemoteStoreMetadataActionTest.java @@ -0,0 +1,160 @@ +/* + * 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.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; +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_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(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()); + } + } + + @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..6eff0da --- /dev/null +++ b/src/test/java/org/codelibs/fesen/client/action/HttpScaleIndexActionTest.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.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 { + + @Test + 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/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..f4d3afe --- /dev/null +++ b/src/test/java/org/codelibs/fesen/client/action/HttpUpdateViewActionTest.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 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.util.List; + +import org.junit.jupiter.api.Test; +import org.opensearch.action.admin.indices.view.CreateViewAction; +import org.opensearch.action.admin.indices.view.UpdateViewAction; + +class HttpUpdateViewActionTest { + + private final HttpUpdateViewAction action = new HttpUpdateViewAction(null, UpdateViewAction.INSTANCE); + + @Test + void test_construction_withNullClient() { + assertNotNull(action); + } + + @Test + 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 = action.buildRequestBody(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_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 = action.buildRequestBody(request); + + assertTrue(source.contains("\"description\":\"Multi-target update\"")); + assertTrue(source.contains("\"index_pattern\":\"logs-*\"")); + assertTrue(source.contains("\"index_pattern\":\"events-*\"")); + } + + @Test + 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 = action.buildRequestBody(request); + + // null description defaults to empty string + assertTrue(source.contains("\"description\":\"\"")); + } +} 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()); + } +}