/* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch licenses this file to you 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.elasticsearch.index.reindex.remote; import org.apache.http.HttpEntity; import org.apache.http.entity.ByteArrayEntity; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; import org.apache.lucene.util.BytesRef; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.Version; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.search.sort.FieldSortBuilder; import org.elasticsearch.search.sort.SortBuilder; import java.io.IOException; import java.util.HashMap; import java.util.Map; import static java.util.Collections.singletonMap; import static org.elasticsearch.common.unit.TimeValue.timeValueMillis; /** * Builds requests for remote version of Elasticsearch. Note that unlike most of the * rest of Elasticsearch this file needs to be compatible with very old versions of * Elasticsearch. Thus is often uses identifiers for versions like {@code 2000099} * for {@code 2.0.0-alpha1}. Do not drop support for features from this file just * because the version constants have been removed. */ final class RemoteRequestBuilders { private RemoteRequestBuilders() {} static String initialSearchPath(SearchRequest searchRequest) { // It is nasty to build paths with StringBuilder but we'll be careful.... StringBuilder path = new StringBuilder("/"); addIndexesOrTypes(path, "Index", searchRequest.indices()); addIndexesOrTypes(path, "Type", searchRequest.types()); path.append("_search"); return path.toString(); } static Map<String, String> initialSearchParams(SearchRequest searchRequest, Version remoteVersion) { Map<String, String> params = new HashMap<>(); if (searchRequest.scroll() != null) { TimeValue keepAlive = searchRequest.scroll().keepAlive(); if (remoteVersion.before(Version.V_5_0_0)) { /* Versions of Elasticsearch before 5.0 couldn't parse nanos or micros * so we toss out that resolution, rounding up because more scroll * timeout seems safer than less. */ keepAlive = timeValueMillis((long) Math.ceil(keepAlive.millisFrac())); } params.put("scroll", keepAlive.getStringRep()); } params.put("size", Integer.toString(searchRequest.source().size())); if (searchRequest.source().version() == null || searchRequest.source().version() == true) { // false is the only value that makes it false. Null defaults to true.... params.put("version", null); } if (searchRequest.source().sorts() != null) { boolean useScan = false; // Detect if we should use search_type=scan rather than a sort if (remoteVersion.before(Version.fromId(2010099))) { for (SortBuilder<?> sort : searchRequest.source().sorts()) { if (sort instanceof FieldSortBuilder) { FieldSortBuilder f = (FieldSortBuilder) sort; if (f.getFieldName().equals(FieldSortBuilder.DOC_FIELD_NAME)) { useScan = true; break; } } } } if (useScan) { params.put("search_type", "scan"); } else { StringBuilder sorts = new StringBuilder(sortToUri(searchRequest.source().sorts().get(0))); for (int i = 1; i < searchRequest.source().sorts().size(); i++) { sorts.append(',').append(sortToUri(searchRequest.source().sorts().get(i))); } params.put("sort", sorts.toString()); } } if (remoteVersion.before(Version.fromId(2000099))) { // Versions before 2.0.0 need prompting to return interesting fields. Note that timestamp isn't available at all.... searchRequest.source().storedField("_parent").storedField("_routing").storedField("_ttl"); if (remoteVersion.before(Version.fromId(1000099))) { // Versions before 1.0.0 don't support `"_source": true` so we have to ask for the _source in a funny way. if (false == searchRequest.source().storedFields().fieldNames().contains("_source")) { searchRequest.source().storedField("_source"); } } } if (searchRequest.source().storedFields() != null && false == searchRequest.source().storedFields().fieldNames().isEmpty()) { StringBuilder fields = new StringBuilder(searchRequest.source().storedFields().fieldNames().get(0)); for (int i = 1; i < searchRequest.source().storedFields().fieldNames().size(); i++) { fields.append(',').append(searchRequest.source().storedFields().fieldNames().get(i)); } String storedFieldsParamName = remoteVersion.before(Version.V_5_0_0_alpha4) ? "fields" : "stored_fields"; params.put(storedFieldsParamName, fields.toString()); } return params; } static HttpEntity initialSearchEntity(SearchRequest searchRequest, BytesReference query, Version remoteVersion) { // EMPTY is safe here because we're not calling namedObject try (XContentBuilder entity = JsonXContent.contentBuilder(); XContentParser queryParser = XContentHelper.createParser(NamedXContentRegistry.EMPTY, query)) { entity.startObject(); entity.field("query"); { /* We're intentionally a bit paranoid here - copying the query as xcontent rather than writing a raw field. We don't want * poorly written queries to escape. Ever. */ entity.copyCurrentStructure(queryParser); XContentParser.Token shouldBeEof = queryParser.nextToken(); if (shouldBeEof != null) { throw new ElasticsearchException( "query was more than a single object. This first token after the object is [" + shouldBeEof + "]"); } } if (searchRequest.source().fetchSource() != null) { entity.field("_source", searchRequest.source().fetchSource()); } else { if (remoteVersion.onOrAfter(Version.fromId(1000099))) { // Versions before 1.0 don't support `"_source": true` so we have to ask for the source as a stored field. entity.field("_source", true); } } entity.endObject(); BytesRef bytes = entity.bytes().toBytesRef(); return new ByteArrayEntity(bytes.bytes, bytes.offset, bytes.length, ContentType.APPLICATION_JSON); } catch (IOException e) { throw new ElasticsearchException("unexpected error building entity", e); } } private static void addIndexesOrTypes(StringBuilder path, String name, String[] indicesOrTypes) { if (indicesOrTypes == null || indicesOrTypes.length == 0) { return; } for (String indexOrType : indicesOrTypes) { checkIndexOrType(name, indexOrType); } path.append(Strings.arrayToCommaDelimitedString(indicesOrTypes)).append('/'); } private static void checkIndexOrType(String name, String indexOrType) { if (indexOrType.indexOf(',') >= 0) { throw new IllegalArgumentException(name + " containing [,] not supported but got [" + indexOrType + "]"); } if (indexOrType.indexOf('/') >= 0) { throw new IllegalArgumentException(name + " containing [/] not supported but got [" + indexOrType + "]"); } } private static String sortToUri(SortBuilder<?> sort) { if (sort instanceof FieldSortBuilder) { FieldSortBuilder f = (FieldSortBuilder) sort; return f.getFieldName() + ":" + f.order(); } throw new IllegalArgumentException("Unsupported sort [" + sort + "]"); } static String scrollPath() { return "/_search/scroll"; } static Map<String, String> scrollParams(TimeValue keepAlive, Version remoteVersion) { if (remoteVersion.before(Version.V_5_0_0)) { /* Versions of Elasticsearch before 5.0 couldn't parse nanos or micros * so we toss out that resolution, rounding up so we shouldn't end up * with 0s. */ keepAlive = timeValueMillis((long) Math.ceil(keepAlive.millisFrac())); } return singletonMap("scroll", keepAlive.getStringRep()); } static HttpEntity scrollEntity(String scroll, Version remoteVersion) { if (remoteVersion.before(Version.fromId(2000099))) { // Versions before 2.0.0 extract the plain scroll_id from the body return new StringEntity(scroll, ContentType.TEXT_PLAIN); } try (XContentBuilder entity = JsonXContent.contentBuilder()) { return new StringEntity(entity.startObject() .field("scroll_id", scroll) .endObject().string(), ContentType.APPLICATION_JSON); } catch (IOException e) { throw new ElasticsearchException("failed to build scroll entity", e); } } static HttpEntity clearScrollEntity(String scroll, Version remoteVersion) { if (remoteVersion.before(Version.fromId(2000099))) { // Versions before 2.0.0 extract the plain scroll_id from the body return new StringEntity(scroll, ContentType.TEXT_PLAIN); } try (XContentBuilder entity = JsonXContent.contentBuilder()) { return new StringEntity(entity.startObject() .array("scroll_id", scroll) .endObject().string(), ContentType.APPLICATION_JSON); } catch (IOException e) { throw new ElasticsearchException("failed to build clear scroll entity", e); } } }