From 959401b9ac6bac5e4d696a750c3e5eb42b7da9f5 Mon Sep 17 00:00:00 2001 From: gus Date: Tue, 23 Jan 2018 14:28:11 -0500 Subject: [PATCH 1/3] SOLR 11617 - patch 2 --- .../org/apache/solr/cloud/ModifyAliasCmd.java | 88 +++++++++++++++++++ .../OverseerCollectionMessageHandler.java | 1 + .../handler/admin/CollectionsHandler.java | 52 ++++++++++- solr/solr-ref-guide/src/collections-api.adoc | 64 ++++++++++++-- .../solrj/request/CollectionApiMapping.java | 11 ++- .../solr/common/params/CollectionParams.java | 1 + .../apispec/collections.Commands.json | 27 ++++++ 7 files changed, 234 insertions(+), 10 deletions(-) create mode 100644 solr/core/src/java/org/apache/solr/cloud/ModifyAliasCmd.java diff --git a/solr/core/src/java/org/apache/solr/cloud/ModifyAliasCmd.java b/solr/core/src/java/org/apache/solr/cloud/ModifyAliasCmd.java new file mode 100644 index 000000000000..d0bf11d0446c --- /dev/null +++ b/solr/core/src/java/org/apache/solr/cloud/ModifyAliasCmd.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF 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.apache.solr.cloud; + +import java.lang.invoke.MethodHandles; +import java.util.Locale; +import java.util.Map; + +import org.apache.solr.common.SolrException; +import org.apache.solr.common.cloud.ClusterState; +import org.apache.solr.common.cloud.ZkNodeProps; +import org.apache.solr.common.cloud.ZkStateReader; +import org.apache.solr.common.util.NamedList; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.apache.solr.cloud.OverseerCollectionMessageHandler.Cmd; +import static org.apache.solr.common.SolrException.ErrorCode.BAD_REQUEST; +import static org.apache.solr.common.params.CommonParams.NAME; + +public class ModifyAliasCmd implements Cmd { + + public static final String META_DATA = "metadata"; + public static final String ALLOW_WHITESPACE_VALUES = "allowWhitespaceValues"; + + private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + private final OverseerCollectionMessageHandler messageHandler; + + ModifyAliasCmd(OverseerCollectionMessageHandler messageHandler) { + this.messageHandler = messageHandler; + } + + @Override + public void call(ClusterState state, ZkNodeProps message, NamedList results) throws Exception { + String aliasName = message.getStr(NAME); + boolean whitespaceValues = message.getBool(ALLOW_WHITESPACE_VALUES, false); + + + ZkStateReader zkStateReader = messageHandler.zkStateReader; + if (zkStateReader.getAliases().getCollectionAliasMap().get(aliasName) == null) { + // nicer than letting aliases object throw later on... + throw new SolrException(BAD_REQUEST, + String.format(Locale.ROOT, "Can't modify non-existent alias %s", aliasName)); + } + + @SuppressWarnings("unchecked") + Map metadata = (Map) message.get(META_DATA); + + zkStateReader.aliasesHolder.applyModificationAndExportToZk(aliases1 -> { + for (String key : metadata.keySet()) { + if ("".equals(key.trim())) { + throw new SolrException(BAD_REQUEST, "metadata keys must not be pure whitespace"); + } + if (!key.equals(key.trim())) { + throw new SolrException(BAD_REQUEST, "metadat keys should not begin or end with whitespace"); + } + String value = metadata.get(key); + if (value != null && !"".equals(value)) { + if (!whitespaceValues && "".equals(value.trim())) { + value = value.trim(); + log.warn("Pure white space value for alias metadata interpreted as null, set allowWhitespaceValue to true if you wish to store a whitespace value, or send null or empty string to avoid this message"); + } + } + if ("".equals(value)) { + value = null; + } + aliases1 = aliases1.cloneWithCollectionAliasMetadata(aliasName, key, value); + } + return aliases1; + }); + } +} diff --git a/solr/core/src/java/org/apache/solr/cloud/OverseerCollectionMessageHandler.java b/solr/core/src/java/org/apache/solr/cloud/OverseerCollectionMessageHandler.java index 426c87960743..6488195f7664 100644 --- a/solr/core/src/java/org/apache/solr/cloud/OverseerCollectionMessageHandler.java +++ b/solr/core/src/java/org/apache/solr/cloud/OverseerCollectionMessageHandler.java @@ -219,6 +219,7 @@ public OverseerCollectionMessageHandler(ZkStateReader zkStateReader, String myId .put(DELETE, new DeleteCollectionCmd(this)) .put(CREATEALIAS, new CreateAliasCmd(this)) .put(DELETEALIAS, new DeleteAliasCmd(this)) + .put(MODIFYALIAS, new ModifyAliasCmd(this)) .put(ROUTEDALIAS_CREATECOLL, new RoutedAliasCreateCollectionCmd(this)) .put(OVERSEERSTATUS, new OverseerStatusCmd(this)) .put(DELETESHARD, new DeleteShardCmd(this)) diff --git a/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java b/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java index 74d47647eef9..f3c8b245ad67 100644 --- a/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java +++ b/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java @@ -133,6 +133,7 @@ import static org.apache.solr.common.params.CommonAdminParams.ASYNC; import static org.apache.solr.common.params.CommonAdminParams.IN_PLACE_MOVE; import static org.apache.solr.common.params.CommonAdminParams.WAIT_FOR_FINAL_STATE; +import static org.apache.solr.cloud.ModifyAliasCmd.ALLOW_WHITESPACE_VALUES; import static org.apache.solr.common.params.CommonParams.NAME; import static org.apache.solr.common.params.CommonParams.VALUE_LONG; import static org.apache.solr.common.params.CoreAdminParams.DATA_DIR; @@ -479,14 +480,35 @@ public enum CollectionOperation implements CollectionOp { DELETEALIAS_OP(DELETEALIAS, (req, rsp, h) -> req.getParams().required().getAll(null, NAME)), /** - * Handle cluster status request. - * Can return status per specific collection/shard or per all collections. + * Change metadata for an alias (use CREATEALIAS_OP to change the actual value of the alias) + */ + MODIFYALIAS_OP(MODIFYALIAS, (req, rsp, h) -> { + Map params = req.getParams().required().getAll(null, NAME); + req.getParams().getAll(params, ALLOW_WHITESPACE_VALUES); + + // Note: success/no-op in the event of no metadata supplied is intentional. Keeps code simple and one less case + // for api-callers to check for. + return convertPrefixToMap(req.getParams(), params, "metadata"); + }), + + /** + * List the aliases and associated metadata. */ LISTALIASES_OP(LISTALIASES, (req, rsp, h) -> { ZkStateReader zkStateReader = h.coreContainer.getZkController().getZkStateReader(); Aliases aliases = zkStateReader.getAliases(); if (aliases != null) { + // the aliases themselves... rsp.getValues().add("aliases", aliases.getCollectionAliasMap()); + // Any metadata for the above aliases. + Map> meta = new LinkedHashMap<>(); + for (String alias : aliases.getCollectionAliasListMap().keySet()) { + Map collectionAliasMetadata = aliases.getCollectionAliasMetadata(alias); + if (collectionAliasMetadata != null) { + meta.put(alias, collectionAliasMetadata); + } + } + rsp.getValues().add("metadata", meta); } return null; }), @@ -931,6 +953,32 @@ public Map execute(SolrQueryRequest req, SolrQueryResponse rsp, "shard"); }), DELETENODE_OP(DELETENODE, (req, rsp, h) -> req.getParams().required().getAll(null, "node")); + + /** + * Places all prefixed properties in the sink map (or a new map) using the prefix as the key and a map of + * all prefixed properties as the value. The sub-map keys have the prefix removed. + * + * @param params The solr params from which to extract prefixed properties. + * @param sink The map to add the properties too. + * @param prefix The prefix to identify properties to be extracted + * @return The sink map, or a new map if the sink map was null + */ + private static Map convertPrefixToMap(SolrParams params, Map sink, String prefix) { + Map result = new LinkedHashMap<>(); + Iterator iter = params.getParameterNamesIterator(); + while (iter.hasNext()) { + String param = iter.next(); + if (param.startsWith(prefix)) { + result.put(param.substring(prefix.length()+1), params.get(param)); + } + } + if (sink == null) { + sink = new LinkedHashMap<>(); + } + sink.put(prefix, result); + return sink; + } + public final CollectionOp fun; CollectionAction action; long timeOut; diff --git a/solr/solr-ref-guide/src/collections-api.adoc b/solr/solr-ref-guide/src/collections-api.adoc index a0c8038951e4..87bd07047c79 100644 --- a/solr/solr-ref-guide/src/collections-api.adoc +++ b/solr/solr-ref-guide/src/collections-api.adoc @@ -542,16 +542,70 @@ http://localhost:8983/solr/admin/collections?action=LISTALIASES&wt=xml *Output* +[source,xml] +---- + + + 0 + 0 + + + collection1 + collection1,collection2 + + + + + someValue + + + +---- + +[[modifyalias]] +== MODIFYALIAS: Modify Alias Metadata for a Collection + +The `MODIFYALIAS` action will Allows changing the metadata on an alias. If a key is set with a value that is empty it will be removed. + +`/admin/collections?action=MODIFYALIAS&name=_name_&metadata.someKey=somevalue` + +=== MODIFYALIAS Parameters + +`name`:: +The alias name on which to set metadata. This parameter is required. + +`metadata.*`:: +The key for the metadata element to be modified replaces '*', the value for the metadata is passed as the value. + +`allowWhitespaceValues`:: +Flag to prevent interpretation of pure whitespace as an empty value. By default this is false and any blank value is treated as empty and removes the corresponding metadata. + +`async`:: +Request ID to track this action which will be <>. + +=== MODIFYALIAS Response + +The output will simply be a responseHeader with details of the time it took to process the request. To confirm the creation of the metadata, you can look in the Solr Admin UI, under the Cloud section and find the `aliases.json` file or use the LISTALIASES api command. + +=== Examples using MODIFYALIAS + +*Input* + +For an alias named "testalias2" and set the value "someValue" for a metadata key of "someKey" and "otherValue" for "otherKey". + +[source,text] +---- +http://localhost:8983/solr/admin/collections?action=MODIFYALIAS&name=testalias2&metadata.someKey=someValue&metadata.otherKey=otherValue&wt=xml +---- + +*Output* + [source,xml] ---- 0 - 0 - - - collection1 - collection2 + 122 ---- diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/request/CollectionApiMapping.java b/solr/solrj/src/java/org/apache/solr/client/solrj/request/CollectionApiMapping.java index 701a45b1326a..f751e3421028 100644 --- a/solr/solrj/src/java/org/apache/solr/client/solrj/request/CollectionApiMapping.java +++ b/solr/solrj/src/java/org/apache/solr/client/solrj/request/CollectionApiMapping.java @@ -54,9 +54,9 @@ import static org.apache.solr.common.params.CollectionParams.CollectionAction.*; import static org.apache.solr.common.params.CommonParams.NAME; -/** stores the mapping of v1 API parameters to v2 API parameters - * for collection API and configset API - * +/** + * Stores the mapping of v1 API parameters to v2 API parameters + * for the collection API and the configset API. */ public class CollectionApiMapping { @@ -119,6 +119,11 @@ public enum Meta implements CommandMeta { DELETEALIAS, "delete-alias", null), + MODIFY_ALIAS(COLLECTIONS_COMMANDS, + POST, + MODIFYALIAS, + "modify-alias", + null), CREATE_SHARD(PER_COLLECTION_SHARDS_COMMANDS, POST, CREATESHARD, diff --git a/solr/solrj/src/java/org/apache/solr/common/params/CollectionParams.java b/solr/solrj/src/java/org/apache/solr/common/params/CollectionParams.java index 9d5fc36bb671..2ed617d26cb2 100644 --- a/solr/solrj/src/java/org/apache/solr/common/params/CollectionParams.java +++ b/solr/solrj/src/java/org/apache/solr/common/params/CollectionParams.java @@ -77,6 +77,7 @@ enum CollectionAction { SYNCSHARD(true, LockLevel.SHARD), CREATEALIAS(true, LockLevel.COLLECTION), DELETEALIAS(true, LockLevel.COLLECTION), + MODIFYALIAS(true, LockLevel.COLLECTION), LISTALIASES(false, LockLevel.NONE), ROUTEDALIAS_CREATECOLL(true, LockLevel.COLLECTION), SPLITSHARD(true, LockLevel.SHARD), diff --git a/solr/solrj/src/resources/apispec/collections.Commands.json b/solr/solrj/src/resources/apispec/collections.Commands.json index 294b1633bbed..8af4b2bf72ba 100644 --- a/solr/solrj/src/resources/apispec/collections.Commands.json +++ b/solr/solrj/src/resources/apispec/collections.Commands.json @@ -168,6 +168,33 @@ }, "required":["name"] }, + "modify-alias": { + "documentation": "https://lucene.apache.org/solr/guide/collections-api.html#modifyalias", + "description": "Allows changing the metadata on an alias. If a key is set with a value that is empty string or pure white space it will be removed", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The alias name on which to set metadata." + }, + "metadata" : { + "type": "object", + "description": "A map of key/value pairs that will be associated with the alias as alias metadata. empty valye will delete any existing valye for a given key", + "additionalProperties": true + }, + "allowWhitespaceValues" : { + "type" : "boolean", + "description" : "If absent or false, any values that are pure whitespace will be equated with null, and unset the metadata value, to intentionally record values with pure whitespace, set this property to true" + }, + "async": { + "type": "string", + "description": "Defines a request ID that can be used to track this action after it's submitted. The action will be processed asynchronously." + } + }, + "required": [ + "name" + ] + }, "backup-collection": { "documentation": "https://lucene.apache.org/solr/guide/collections-api.html#backup", "description": "Backup Solr indexes and configurations for a specific collection. One copy of the indexes will be taken from each shard, and the config set for the collection will also be copied.", From 4b4c656a107eba82ec1bfe11e8f5b14b1ea751bf Mon Sep 17 00:00:00 2001 From: Gus Heck Date: Tue, 23 Jan 2018 22:44:46 -0500 Subject: [PATCH 2/3] adjust for package changes in master --- .../solr/cloud/{ => api/collections}/ModifyAliasCmd.java | 4 ++-- .../org/apache/solr/handler/admin/CollectionsHandler.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename solr/core/src/java/org/apache/solr/cloud/{ => api/collections}/ModifyAliasCmd.java (96%) diff --git a/solr/core/src/java/org/apache/solr/cloud/ModifyAliasCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/ModifyAliasCmd.java similarity index 96% rename from solr/core/src/java/org/apache/solr/cloud/ModifyAliasCmd.java rename to solr/core/src/java/org/apache/solr/cloud/api/collections/ModifyAliasCmd.java index d0bf11d0446c..ff5b70b227f5 100644 --- a/solr/core/src/java/org/apache/solr/cloud/ModifyAliasCmd.java +++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/ModifyAliasCmd.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.solr.cloud; +package org.apache.solr.cloud.api.collections; import java.lang.invoke.MethodHandles; import java.util.Locale; @@ -29,7 +29,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static org.apache.solr.cloud.OverseerCollectionMessageHandler.Cmd; +import static org.apache.solr.cloud.api.collections.OverseerCollectionMessageHandler.*; import static org.apache.solr.common.SolrException.ErrorCode.BAD_REQUEST; import static org.apache.solr.common.params.CommonParams.NAME; diff --git a/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java b/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java index 3b3a741a1cb9..983274bf636e 100644 --- a/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java +++ b/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java @@ -133,7 +133,7 @@ import static org.apache.solr.common.params.CommonAdminParams.ASYNC; import static org.apache.solr.common.params.CommonAdminParams.IN_PLACE_MOVE; import static org.apache.solr.common.params.CommonAdminParams.WAIT_FOR_FINAL_STATE; -import static org.apache.solr.cloud.ModifyAliasCmd.ALLOW_WHITESPACE_VALUES; +import static org.apache.solr.cloud.api.collections.ModifyAliasCmd.ALLOW_WHITESPACE_VALUES; import static org.apache.solr.common.params.CommonParams.NAME; import static org.apache.solr.common.params.CommonParams.VALUE_LONG; import static org.apache.solr.common.params.CoreAdminParams.DATA_DIR; From db825f31e16ff5d85c58e1e62002b707392dcfe2 Mon Sep 17 00:00:00 2001 From: Gus Heck Date: Sun, 28 Jan 2018 01:50:39 -0500 Subject: [PATCH 3/3] Remove whitespace param, add tests --- .../cloud/api/collections/ModifyAliasCmd.java | 10 +- .../handler/admin/CollectionsHandler.java | 2 - .../solr/cloud/AliasIntegrationTest.java | 161 ++++++++++++++++-- solr/solr-ref-guide/src/collections-api.adoc | 5 +- .../solrj/request/CollectionAdminRequest.java | 35 ++++ .../apispec/collections.Commands.json | 6 +- 6 files changed, 187 insertions(+), 32 deletions(-) diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/ModifyAliasCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/ModifyAliasCmd.java index ff5b70b227f5..2b6bd1ee14bc 100644 --- a/solr/core/src/java/org/apache/solr/cloud/api/collections/ModifyAliasCmd.java +++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/ModifyAliasCmd.java @@ -36,7 +36,6 @@ public class ModifyAliasCmd implements Cmd { public static final String META_DATA = "metadata"; - public static final String ALLOW_WHITESPACE_VALUES = "allowWhitespaceValues"; private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); @@ -49,7 +48,6 @@ public class ModifyAliasCmd implements Cmd { @Override public void call(ClusterState state, ZkNodeProps message, NamedList results) throws Exception { String aliasName = message.getStr(NAME); - boolean whitespaceValues = message.getBool(ALLOW_WHITESPACE_VALUES, false); ZkStateReader zkStateReader = messageHandler.zkStateReader; @@ -68,15 +66,9 @@ public void call(ClusterState state, ZkNodeProps message, NamedList results) thr throw new SolrException(BAD_REQUEST, "metadata keys must not be pure whitespace"); } if (!key.equals(key.trim())) { - throw new SolrException(BAD_REQUEST, "metadat keys should not begin or end with whitespace"); + throw new SolrException(BAD_REQUEST, "metadata keys should not begin or end with whitespace"); } String value = metadata.get(key); - if (value != null && !"".equals(value)) { - if (!whitespaceValues && "".equals(value.trim())) { - value = value.trim(); - log.warn("Pure white space value for alias metadata interpreted as null, set allowWhitespaceValue to true if you wish to store a whitespace value, or send null or empty string to avoid this message"); - } - } if ("".equals(value)) { value = null; } diff --git a/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java b/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java index 6f9b048ff6de..cebb2d057d4a 100644 --- a/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java +++ b/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java @@ -138,7 +138,6 @@ import static org.apache.solr.common.params.CommonAdminParams.ASYNC; import static org.apache.solr.common.params.CommonAdminParams.IN_PLACE_MOVE; import static org.apache.solr.common.params.CommonAdminParams.WAIT_FOR_FINAL_STATE; -import static org.apache.solr.cloud.api.collections.ModifyAliasCmd.ALLOW_WHITESPACE_VALUES; import static org.apache.solr.common.params.CommonParams.NAME; import static org.apache.solr.common.params.CommonParams.VALUE_LONG; import static org.apache.solr.common.params.CoreAdminParams.DATA_DIR; @@ -542,7 +541,6 @@ public enum CollectionOperation implements CollectionOp { */ MODIFYALIAS_OP(MODIFYALIAS, (req, rsp, h) -> { Map params = req.getParams().required().getAll(null, NAME); - req.getParams().getAll(params, ALLOW_WHITESPACE_VALUES); // Note: success/no-op in the event of no metadata supplied is intentional. Keeps code simple and one less case // for api-callers to check for. diff --git a/solr/core/src/test/org/apache/solr/cloud/AliasIntegrationTest.java b/solr/core/src/test/org/apache/solr/cloud/AliasIntegrationTest.java index 1c20b30a5e4d..e2e155ffa5f3 100644 --- a/solr/core/src/test/org/apache/solr/cloud/AliasIntegrationTest.java +++ b/solr/core/src/test/org/apache/solr/cloud/AliasIntegrationTest.java @@ -22,6 +22,15 @@ import java.util.function.Consumer; import java.util.function.UnaryOperator; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.util.EntityUtils; +import org.apache.lucene.util.IOUtils; import org.apache.solr.client.solrj.SolrQuery; import org.apache.solr.client.solrj.SolrRequest; import org.apache.solr.client.solrj.SolrServerException; @@ -40,6 +49,9 @@ import org.apache.solr.common.params.ModifiableSolrParams; import org.apache.solr.common.params.SolrParams; import org.apache.solr.common.util.Utils; +import org.apache.zookeeper.KeeperException; +import org.junit.After; +import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; @@ -47,6 +59,9 @@ public class AliasIntegrationTest extends SolrCloudTestCase { + private CloseableHttpClient httpClient; + private CloudSolrClient solrClient; + @BeforeClass public static void setupCluster() throws Exception { configureCluster(2) @@ -54,6 +69,33 @@ public static void setupCluster() throws Exception { .configure(); } + @Before + @Override + public void setUp() throws Exception { + super.setUp(); + solrClient = getCloudSolrClient(cluster); + httpClient = (CloseableHttpClient) solrClient.getHttpClient(); + } + + @After + @Override + public void tearDown() throws Exception { + super.tearDown(); + IOUtils.close(solrClient, httpClient); + + // make sure all aliases created are removed for the next test method + Map aliases = new CollectionAdminRequest.ListAliases().process(cluster.getSolrClient()).getAliases(); + for (String alias : aliases.keySet()) { + CollectionAdminRequest.deleteAlias(alias).processAsync(cluster.getSolrClient()); + } + + // make sure all collections are removed for the next test method + List collections = CollectionAdminRequest.listCollections(cluster.getSolrClient()); + for (String collection : collections) { + CollectionAdminRequest.deleteCollection(collection).process(cluster.getSolrClient()); + } + } + @Test public void testMetadata() throws Exception { CollectionAdminRequest.createCollection("collection1meta", "conf", 2, 1).process(cluster.getSolrClient()); @@ -75,6 +117,7 @@ public void testMetadata() throws Exception { assertEquals("collection2meta", aliases.get(1)); //ensure we have the back-compat format in ZK: final byte[] rawBytes = zkStateReader.getZkClient().getData(ALIASES, null, null, true); + //noinspection unchecked assertTrue(((Map>)Utils.fromJSON(rawBytes)).get("collection").get("meta1") instanceof String); // set metadata @@ -178,6 +221,104 @@ public void testMetadata() throws Exception { } } + public void testModifyMetadataV2() throws Exception { + final String aliasName = getTestName(); + ZkStateReader zkStateReader = createColectionsAndAlias(aliasName); + final String baseUrl = cluster.getRandomJetty(random()).getBaseUrl().toString(); + //TODO fix Solr test infra so that this /____v2/ becomes /api/ + HttpPost post = new HttpPost(baseUrl + "/____v2/c"); + post.setEntity(new StringEntity("{\n" + + "\"modify-alias\" : {\n" + + " \"name\": \"" + aliasName + "\",\n" + + " \"metadata\" : {\n" + + " \"foo\": \"baz\",\n" + + " \"bar\": \"bam\"\n" + + " }\n" + + //TODO should we use "NOW=" param? Won't work with v2 and is kinda a hack any way since intended for distrib + " }\n" + + "}", ContentType.APPLICATION_JSON)); + assertSuccess(post); + checkFooAndBarMeta(aliasName, zkStateReader); + } + + public void testModifyMetadataV1() throws Exception { + // note we don't use TZ in this test, thus it's UTC + final String aliasName = getTestName(); + ZkStateReader zkStateReader = createColectionsAndAlias(aliasName); + final String baseUrl = cluster.getRandomJetty(random()).getBaseUrl().toString(); + HttpGet get = new HttpGet(baseUrl + "/admin/collections?action=MODIFYALIAS" + + "&wt=xml" + + "&name=" + aliasName + + "&metadata.foo=baz" + + "&metadata.bar=bam"); + assertSuccess(get); + checkFooAndBarMeta(aliasName, zkStateReader); + } + + public void testModifyMetadataCAR() throws Exception { + // note we don't use TZ in this test, thus it's UTC + final String aliasName = getTestName(); + ZkStateReader zkStateReader = createColectionsAndAlias(aliasName); + CollectionAdminRequest.ModifyAlias modifyAlias = CollectionAdminRequest.modifyAlias(aliasName); + modifyAlias.addMetadata("foo","baz"); + modifyAlias.addMetadata("bar","bam"); + modifyAlias.process(cluster.getSolrClient()); + checkFooAndBarMeta(aliasName, zkStateReader); + + // now verify we can delete + modifyAlias = CollectionAdminRequest.modifyAlias(aliasName); + modifyAlias.addMetadata("foo",""); + modifyAlias.process(cluster.getSolrClient()); + modifyAlias = CollectionAdminRequest.modifyAlias(aliasName); + modifyAlias.addMetadata("bar",null); + modifyAlias.process(cluster.getSolrClient()); + modifyAlias = CollectionAdminRequest.modifyAlias(aliasName); + + // whitespace value + modifyAlias.addMetadata("foo"," "); + modifyAlias.process(cluster.getSolrClient()); + + + } + + private void checkFooAndBarMeta(String aliasName, ZkStateReader zkStateReader) { + Map meta = zkStateReader.getAliases().getCollectionAliasMetadata(aliasName); + assertNotNull(meta); + assertTrue(meta.containsKey("foo")); + assertEquals("baz", meta.get("foo")); + assertTrue(meta.containsKey("bar")); + assertEquals("bam", meta.get("bar")); + } + + private ZkStateReader createColectionsAndAlias(String aliasName) throws SolrServerException, IOException, KeeperException, InterruptedException { + CollectionAdminRequest.createCollection("collection1meta", "conf", 2, 1).process(cluster.getSolrClient()); + CollectionAdminRequest.createCollection("collection2meta", "conf", 1, 1).process(cluster.getSolrClient()); + waitForState("Expected collection1 to be created with 2 shards and 1 replica", "collection1meta", clusterShape(2, 1)); + waitForState("Expected collection2 to be created with 1 shard and 1 replica", "collection2meta", clusterShape(1, 1)); + ZkStateReader zkStateReader = cluster.getSolrClient().getZkStateReader(); + zkStateReader.createClusterStateWatchersAndUpdate(); + List aliases = zkStateReader.getAliases().resolveAliases(aliasName); + assertEquals(1, aliases.size()); + assertEquals(aliasName, aliases.get(0)); + UnaryOperator op6 = a -> a.cloneWithCollectionAlias(aliasName, "collection1meta,collection2meta"); + final ZkStateReader.AliasesManager aliasesHolder = zkStateReader.aliasesHolder; + + aliasesHolder.applyModificationAndExportToZk(op6); + aliases = zkStateReader.getAliases().resolveAliases(aliasName); + assertEquals(2, aliases.size()); + assertEquals("collection1meta", aliases.get(0)); + assertEquals("collection2meta", aliases.get(1)); + return zkStateReader; + } + + private void assertSuccess(HttpUriRequest msg) throws IOException { + try (CloseableHttpResponse response = httpClient.execute(msg)) { + if (200 != response.getStatusLine().getStatusCode()) { + System.err.println(EntityUtils.toString(response.getEntity())); + fail("Unexpected status: " + response.getStatusLine()); + } + } + } // Rather a long title, but it's common to recommend when people need to re-index for any reason that they: // 1> create a new collection // 2> index the corpus to the new collection and verify it @@ -497,22 +638,19 @@ public void testErrorChecks() throws Exception { ignoreException("."); // Invalid Alias name - SolrException e = expectThrows(SolrException.class, () -> { - CollectionAdminRequest.createAlias("test:alias", "testErrorChecks-collection").process(cluster.getSolrClient()); - }); + SolrException e = expectThrows(SolrException.class, () -> + CollectionAdminRequest.createAlias("test:alias", "testErrorChecks-collection").process(cluster.getSolrClient())); assertEquals(SolrException.ErrorCode.BAD_REQUEST, SolrException.ErrorCode.getErrorCode(e.code())); // Target collection doesn't exists - e = expectThrows(SolrException.class, () -> { - CollectionAdminRequest.createAlias("testalias", "doesnotexist").process(cluster.getSolrClient()); - }); + e = expectThrows(SolrException.class, () -> + CollectionAdminRequest.createAlias("testalias", "doesnotexist").process(cluster.getSolrClient())); assertEquals(SolrException.ErrorCode.BAD_REQUEST, SolrException.ErrorCode.getErrorCode(e.code())); assertTrue(e.getMessage().contains("Can't create collection alias for collections='doesnotexist', 'doesnotexist' is not an existing collection or alias")); // One of the target collections doesn't exist - e = expectThrows(SolrException.class, () -> { - CollectionAdminRequest.createAlias("testalias", "testErrorChecks-collection,doesnotexist").process(cluster.getSolrClient()); - }); + e = expectThrows(SolrException.class, () -> + CollectionAdminRequest.createAlias("testalias", "testErrorChecks-collection,doesnotexist").process(cluster.getSolrClient())); assertEquals(SolrException.ErrorCode.BAD_REQUEST, SolrException.ErrorCode.getErrorCode(e.code())); assertTrue(e.getMessage().contains("Can't create collection alias for collections='testErrorChecks-collection,doesnotexist', 'doesnotexist' is not an existing collection or alias")); @@ -522,9 +660,8 @@ public void testErrorChecks() throws Exception { CollectionAdminRequest.createAlias("testalias2", "testalias").process(cluster.getSolrClient()); // Alias + invalid - e = expectThrows(SolrException.class, () -> { - CollectionAdminRequest.createAlias("testalias3", "testalias2,doesnotexist").process(cluster.getSolrClient()); - }); + e = expectThrows(SolrException.class, () -> + CollectionAdminRequest.createAlias("testalias3", "testalias2,doesnotexist").process(cluster.getSolrClient())); assertEquals(SolrException.ErrorCode.BAD_REQUEST, SolrException.ErrorCode.getErrorCode(e.code())); unIgnoreException("."); diff --git a/solr/solr-ref-guide/src/collections-api.adoc b/solr/solr-ref-guide/src/collections-api.adoc index 305f48166a94..692d64d048d8 100644 --- a/solr/solr-ref-guide/src/collections-api.adoc +++ b/solr/solr-ref-guide/src/collections-api.adoc @@ -738,10 +738,7 @@ The `MODIFYALIAS` action will Allows changing the metadata on an alias. If a key The alias name on which to set metadata. This parameter is required. `metadata.*`:: -The key for the metadata element to be modified replaces '*', the value for the metadata is passed as the value. - -`allowWhitespaceValues`:: -Flag to prevent interpretation of pure whitespace as an empty value. By default this is false and any blank value is treated as empty and removes the corresponding metadata. +The name of the key for the metadata element to be modified replaces '*', the value for the parameter is passed as the value for the metadata. `async`:: Request ID to track this action which will be <>. diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/request/CollectionAdminRequest.java b/solr/solrj/src/java/org/apache/solr/client/solrj/request/CollectionAdminRequest.java index e2e354020166..a3d5ecbd3958 100644 --- a/solr/solrj/src/java/org/apache/solr/client/solrj/request/CollectionAdminRequest.java +++ b/solr/solrj/src/java/org/apache/solr/client/solrj/request/CollectionAdminRequest.java @@ -17,6 +17,7 @@ package org.apache.solr.client.solrj.request; import java.io.IOException; +import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Optional; @@ -1319,6 +1320,40 @@ protected CollectionAdminResponse createResponse(SolrClient client) { } + // MODIFYALIAS request + + /** + * Returns a SolrRequest to add or remove metadata from a request + * @param aliasName the alias to modify + */ + + public static ModifyAlias modifyAlias(String aliasName) { + return new ModifyAlias(aliasName); + } + + public static class ModifyAlias extends AsyncCollectionAdminRequest { + + private final String aliasName; + private static Map metadata = new HashMap<>(); + + public ModifyAlias(String aliasName) { + super(CollectionAction.MODIFYALIAS); + this.aliasName = SolrIdentifierValidator.validateAliasName(aliasName); + } + + public void addMetadata(String key, String value) { + metadata.put(key,value); + } + + @Override + public SolrParams getParams() { + ModifiableSolrParams params = (ModifiableSolrParams) super.getParams(); + params.set(CoreAdminParams.NAME, aliasName); + metadata.keySet().forEach(key -> params.set("metadata." + key, metadata.get(key))); + return params; + } + } + /** * Returns a SolrRequest to create a new alias * @param aliasName the alias name diff --git a/solr/solrj/src/resources/apispec/collections.Commands.json b/solr/solrj/src/resources/apispec/collections.Commands.json index b07b0518c7b1..e47aab6ceb7e 100644 --- a/solr/solrj/src/resources/apispec/collections.Commands.json +++ b/solr/solrj/src/resources/apispec/collections.Commands.json @@ -238,13 +238,9 @@ }, "metadata" : { "type": "object", - "description": "A map of key/value pairs that will be associated with the alias as alias metadata. empty valye will delete any existing valye for a given key", + "description": "A map of key/value pairs that will be associated with the alias as alias metadata. Empty value will delete any existing value for a given key", "additionalProperties": true }, - "allowWhitespaceValues" : { - "type" : "boolean", - "description" : "If absent or false, any values that are pure whitespace will be equated with null, and unset the metadata value, to intentionally record values with pure whitespace, set this property to true" - }, "async": { "type": "string", "description": "Defines a request ID that can be used to track this action after it's submitted. The action will be processed asynchronously."