/* * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ package org.opensearch.search.asynchronous.response; import org.opensearch.BaseExceptionsHelper; import org.opensearch.core.ParseField; import org.opensearch.core.xcontent.ToXContent; import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.core.xcontent.XContentParser; import org.opensearch.search.asynchronous.context.state.AsynchronousSearchState; import org.opensearch.OpenSearchException; import org.opensearch.ExceptionsHelper; import org.opensearch.action.ActionResponse; import org.opensearch.action.search.SearchResponse; import org.opensearch.client.Requests; import org.opensearch.common.Nullable; import org.opensearch.common.Strings; import org.opensearch.common.bytes.BytesReference; import org.opensearch.common.io.stream.StreamInput; import org.opensearch.common.io.stream.StreamOutput; import org.opensearch.common.xcontent.StatusToXContentObject; import org.opensearch.common.xcontent.XContentFactory; import org.opensearch.common.xcontent.XContentHelper; import org.opensearch.common.xcontent.XContentType; import org.opensearch.rest.RestStatus; import java.io.IOException; import java.util.Map; import java.util.Objects; import static java.util.Collections.emptyMap; import static org.opensearch.common.xcontent.XContentHelper.convertToMap; import static org.opensearch.common.xcontent.XContentParserUtils.ensureExpectedToken; public class AsynchronousSearchResponse extends ActionResponse implements StatusToXContentObject { private static final ParseField ID = new ParseField("id"); private static final ParseField STATE = new ParseField("state"); private static final ParseField START_TIME_IN_MILLIS = new ParseField("start_time_in_millis"); private static final ParseField EXPIRATION_TIME_IN_MILLIS = new ParseField("expiration_time_in_millis"); private static final ParseField RESPONSE = new ParseField("response"); private static final ParseField ERROR = new ParseField("error"); @Nullable //when the search is cancelled we don't have the id private final String id; private final AsynchronousSearchState state; private final long startTimeMillis; private final long expirationTimeMillis; @Nullable private SearchResponse searchResponse; @Nullable private OpenSearchException error; public AsynchronousSearchResponse(String id, AsynchronousSearchState state, long startTimeMillis, long expirationTimeMillis, SearchResponse searchResponse, Exception error) { this.id = id; this.state = state; this.startTimeMillis = startTimeMillis; this.expirationTimeMillis = expirationTimeMillis; this.searchResponse = searchResponse; this.error = error == null ? null : ExceptionsHelper.convertToOpenSearchException(error); } public AsynchronousSearchResponse(String id, AsynchronousSearchState state, long startTimeMillis, long expirationTimeMillis, SearchResponse searchResponse, OpenSearchException error) { this.id = id; this.state = state; this.startTimeMillis = startTimeMillis; this.expirationTimeMillis = expirationTimeMillis; this.searchResponse = searchResponse; this.error = error; } public AsynchronousSearchResponse(AsynchronousSearchState state, long startTimeMillis, long expirationTimeMillis, SearchResponse searchResponse, OpenSearchException error) { this.state = state; this.startTimeMillis = startTimeMillis; this.expirationTimeMillis = expirationTimeMillis; this.searchResponse = searchResponse; this.error = error; this.id = null; } public AsynchronousSearchResponse(StreamInput in) throws IOException { this.id = in.readOptionalString(); this.state = in.readEnum(AsynchronousSearchState.class); this.startTimeMillis = in.readLong(); this.expirationTimeMillis = in.readLong(); this.searchResponse = in.readOptionalWriteable(SearchResponse::new); this.error = in.readBoolean() ? in.readException() : null; } @Override public void writeTo(StreamOutput out) throws IOException { out.writeOptionalString(id); out.writeEnum(state); out.writeLong(startTimeMillis); out.writeLong(expirationTimeMillis); out.writeOptionalWriteable(searchResponse); if (error != null) { out.writeBoolean(true); out.writeException(error); } else { out.writeBoolean(false); } } @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); if (id != null) { builder.field(ID.getPreferredName(), id); } builder.field(STATE.getPreferredName(), state); builder.field(START_TIME_IN_MILLIS.getPreferredName(), startTimeMillis); builder.field(EXPIRATION_TIME_IN_MILLIS.getPreferredName(), expirationTimeMillis); if (searchResponse != null) { builder.startObject(RESPONSE.getPreferredName()); searchResponse.innerToXContent(builder, params); builder.endObject(); } if (error != null) { builder.startObject(ERROR.getPreferredName()); BaseExceptionsHelper.generateThrowableXContent(builder, ToXContent.EMPTY_PARAMS, error); builder.endObject(); } builder.endObject(); return builder; } public AsynchronousSearchState getState() { return state; } public long getStartTimeMillis() { return startTimeMillis; } public SearchResponse getSearchResponse() { return searchResponse; } public OpenSearchException getError() { return error; } public long getExpirationTimeMillis() { return expirationTimeMillis; } public String getId() { return id; } @Override public RestStatus status() { return RestStatus.OK; } @Override public String toString() { return Strings.toString(XContentType.JSON, this); } /** * {@linkplain SearchResponse} and {@linkplain OpenSearchException} don't override hashcode, hence cannot be included in * the hashcode calculation for {@linkplain AsynchronousSearchResponse}. Given that we are using these methods only in tests; on the * off-chance that the {@link #equals(Object)} ()} comparison fails and hashcode is equal for 2 * {@linkplain AsynchronousSearchResponse} objects, we are wary of the @see * * performance improvement on hash tables } that we forgo. * * @return hashcode of {@linkplain AsynchronousSearchResponse} */ @Override public int hashCode() { return Objects.hash(id, state, startTimeMillis, expirationTimeMillis); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } AsynchronousSearchResponse other = (AsynchronousSearchResponse) o; try { return ((id == null && other.id == null) || (id != null && id.equals(other.id))) && state.equals(other.state) && startTimeMillis == other.startTimeMillis && expirationTimeMillis == other.expirationTimeMillis && Objects.equals(getErrorAsMap(error), getErrorAsMap(other.error)) && Objects.equals(getResponseAsMap(searchResponse), getResponseAsMap(other.searchResponse)); } catch (IOException e) { return false; } } private Map getErrorAsMap(OpenSearchException exception) throws IOException { if (exception != null) { BytesReference error; try (XContentBuilder builder = XContentFactory.contentBuilder(Requests.INDEX_CONTENT_TYPE)) { builder.startObject(); BaseExceptionsHelper.generateThrowableXContent(builder, ToXContent.EMPTY_PARAMS, exception); builder.endObject(); error = BytesReference.bytes(builder); return convertToMap(error, false, Requests.INDEX_CONTENT_TYPE).v2(); } } else { return emptyMap(); } } private Map getResponseAsMap(SearchResponse searchResponse) throws IOException { if (searchResponse != null) { BytesReference response = XContentHelper.toXContent(searchResponse, Requests.INDEX_CONTENT_TYPE, true); if (response == null) { return emptyMap(); } return convertToMap(response, false, Requests.INDEX_CONTENT_TYPE).v2(); } else { return null; } } public static AsynchronousSearchResponse fromXContent(XContentParser parser) throws IOException { ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser); parser.nextToken(); return innerFromXContent(parser); } public static AsynchronousSearchResponse innerFromXContent(XContentParser parser) throws IOException { ensureExpectedToken(XContentParser.Token.FIELD_NAME, parser.currentToken(), parser); String id = null; AsynchronousSearchState status = null; long startTimeMillis = -1; long expirationTimeMillis = -1; SearchResponse searchResponse = null; OpenSearchException error = null; String currentFieldName = null; for (XContentParser.Token token = parser.nextToken(); token != XContentParser.Token.END_OBJECT; token = parser.nextToken()) { currentFieldName = parser.currentName(); if (RESPONSE.match(currentFieldName, parser.getDeprecationHandler())) { if (token == XContentParser.Token.START_OBJECT) { ensureExpectedToken(XContentParser.Token.FIELD_NAME, parser.nextToken(), parser); searchResponse = SearchResponse.innerFromXContent(parser); } } else if (ERROR.match(currentFieldName, parser.getDeprecationHandler())) { parser.nextToken(); error = OpenSearchException.fromXContent(parser); } else if (token.isValue()) { if (ID.match(currentFieldName, parser.getDeprecationHandler())) { id = parser.text(); } else if (START_TIME_IN_MILLIS.match(currentFieldName, parser.getDeprecationHandler())) { startTimeMillis = parser.longValue(); } else if (EXPIRATION_TIME_IN_MILLIS.match(currentFieldName, parser.getDeprecationHandler())) { expirationTimeMillis = parser.longValue(); } else if (STATE.match(currentFieldName, parser.getDeprecationHandler())) { status = AsynchronousSearchState.valueOf(parser.text()); } else { parser.skipChildren(); } } } return new AsynchronousSearchResponse(id, status, startTimeMillis, expirationTimeMillis, searchResponse, error); } //visible for testing public static AsynchronousSearchResponse empty(String id, SearchResponse searchResponse, Exception exception) { return new AsynchronousSearchResponse(id, null, -1, -1, searchResponse, exception); } }