From 73d5e7ae77d8953cb9be35a7cbcebe3a516dd04a Mon Sep 17 00:00:00 2001 From: Noble Paul Date: Tue, 17 Nov 2020 00:19:50 +1100 Subject: [PATCH] SOLR-14977 : ContainerPlugins should be configurable (#2065) --- solr/CHANGES.txt | 2 + .../org/apache/solr/api/AnnotatedApi.java | 5 +- .../apache/solr/api/ConfigurablePlugin.java | 33 +++++ .../solr/api/ContainerPluginsRegistry.java | 88 +++++++++++-- .../java/org/apache/solr/api/V2HttpCall.java | 2 +- .../handler/admin/ContainerPluginsApi.java | 18 +-- .../solr/handler/TestContainerPlugin.java | 124 +++++++++++++++--- .../solr/client/solrj/request/V2Request.java | 20 +++ .../solr/common/util/JsonSchemaCreator.java | 1 + 9 files changed, 251 insertions(+), 42 deletions(-) create mode 100644 solr/core/src/java/org/apache/solr/api/ConfigurablePlugin.java diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt index 28c9ab43370..53aff7c0a48 100644 --- a/solr/CHANGES.txt +++ b/solr/CHANGES.txt @@ -176,6 +176,8 @@ Improvements * SOLR-14683: Metrics API should ensure consistent placeholders for missing values. (ab) +* SOLR-14977 : ContainerPlugins should be configurable with custom objects (noble, ab) + Optimizations --------------------- * SOLR-14975: Optimize CoreContainer.getAllCoreNames, getLoadedCoreNames and getCoreDescriptors. (Bruno Roustant) diff --git a/solr/core/src/java/org/apache/solr/api/AnnotatedApi.java b/solr/core/src/java/org/apache/solr/api/AnnotatedApi.java index f9f97a4a7f9..9ec86ce7595 100644 --- a/solr/core/src/java/org/apache/solr/api/AnnotatedApi.java +++ b/solr/core/src/java/org/apache/solr/api/AnnotatedApi.java @@ -33,6 +33,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.solr.client.solrj.SolrRequest; import org.apache.solr.common.SolrException; @@ -62,6 +63,8 @@ import org.slf4j.LoggerFactory; public class AnnotatedApi extends Api implements PermissionNameProvider , Closeable { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + private static final ObjectMapper mapper = SolrJacksonAnnotationInspector.createObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); public static final String ERR = "Error executing commands :"; private EndPoint endPoint; @@ -222,7 +225,7 @@ public class AnnotatedApi extends Api implements PermissionNameProvider , Closea final String command; final MethodHandle method; final Object obj; - ObjectMapper mapper = SolrJacksonAnnotationInspector.createObjectMapper(); + int paramsCount; @SuppressWarnings({"rawtypes"}) Class parameterClass; diff --git a/solr/core/src/java/org/apache/solr/api/ConfigurablePlugin.java b/solr/core/src/java/org/apache/solr/api/ConfigurablePlugin.java new file mode 100644 index 00000000000..ef13d8a312a --- /dev/null +++ b/solr/core/src/java/org/apache/solr/api/ConfigurablePlugin.java @@ -0,0 +1,33 @@ +/* + * 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.api; + +import org.apache.solr.common.MapWriter; + +/**Implement this interface if your plugin needs to accept some configuration + * + * @param the configuration Object type + */ +public interface ConfigurablePlugin { + + /**This is invoked soon after the Object is initialized + * + * @param cfg value deserialized from JSON + */ + void configure(T cfg); +} diff --git a/solr/core/src/java/org/apache/solr/api/ContainerPluginsRegistry.java b/solr/core/src/java/org/apache/solr/api/ContainerPluginsRegistry.java index 8d0267c8a6f..158bcf63b36 100644 --- a/solr/core/src/java/org/apache/solr/api/ContainerPluginsRegistry.java +++ b/solr/core/src/java/org/apache/solr/api/ContainerPluginsRegistry.java @@ -22,6 +22,8 @@ import java.io.IOException; import java.lang.invoke.MethodHandles; import java.lang.reflect.Constructor; import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -31,6 +33,7 @@ import java.util.Optional; import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Supplier; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.lucene.util.ResourceLoaderAware; import org.apache.solr.client.solrj.SolrRequest; @@ -69,7 +72,8 @@ import static org.apache.solr.common.util.Utils.makeMap; public class ContainerPluginsRegistry implements ClusterPropertiesListener, MapWriter, Closeable { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - private final ObjectMapper mapper = SolrJacksonAnnotationInspector.createObjectMapper(); + private static final ObjectMapper mapper = SolrJacksonAnnotationInspector.createObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); private final List listeners = new CopyOnWriteArrayList<>(); @@ -114,6 +118,30 @@ public class ContainerPluginsRegistry implements ClusterPropertiesListener, MapW return currentPlugins.get(name); } + static class PluginMetaHolder { + private final Map original; + private final PluginMeta meta; + + PluginMetaHolder(Map original) throws IOException { + this.original = original; + meta = mapper.readValue(Utils.toJSON(original), PluginMeta.class); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof PluginMetaHolder) { + PluginMetaHolder that = (PluginMetaHolder) obj; + return Objects.equals(this.original,that.original); + } + return false; + } + + @Override + public int hashCode() { + return original.hashCode(); + } + } + @SuppressWarnings("unchecked") public synchronized void refresh() { Map pluginInfos = null; try { @@ -122,19 +150,18 @@ public class ContainerPluginsRegistry implements ClusterPropertiesListener, MapW log.error("Could not read plugins data", e); return; } - Map newState = new HashMap<>(pluginInfos.size()); + Map newState = new HashMap<>(pluginInfos.size()); for (Map.Entry e : pluginInfos.entrySet()) { try { - newState.put(e.getKey(), - mapper.readValue(Utils.toJSON(e.getValue()), PluginMeta.class)); + newState.put(e.getKey(),new PluginMetaHolder((Map) e.getValue())); } catch (Exception exp) { log.error("Invalid apiInfo configuration :", exp); } } - Map currentState = new HashMap<>(); + Map currentState = new HashMap<>(); for (Map.Entry e : currentPlugins.entrySet()) { - currentState.put(e.getKey(), e.getValue().info); + currentState.put(e.getKey(), e.getValue().holder); } Map diff = compareMaps(currentState, newState); if (diff == null) return;//nothing has changed @@ -153,10 +180,10 @@ public class ContainerPluginsRegistry implements ClusterPropertiesListener, MapW } } else { //ADDED or UPDATED - PluginMeta info = newState.get(e.getKey()); + PluginMetaHolder info = newState.get(e.getKey()); ApiInfo apiInfo = null; List errs = new ArrayList<>(); - apiInfo = new ApiInfo(info, errs); + apiInfo = new ApiInfo(info,errs); if (!errs.isEmpty()) { log.error(StrUtils.join(errs, ',')); continue; @@ -243,8 +270,10 @@ public class ContainerPluginsRegistry implements ClusterPropertiesListener, MapW public class ApiInfo implements ReflectMapWriter { List holders; + private final PluginMetaHolder holder; + @JsonProperty - private final PluginMeta info; + private PluginMeta info; @JsonProperty(value = "package") public final String pkg; @@ -272,8 +301,9 @@ public class ContainerPluginsRegistry implements ClusterPropertiesListener, MapW return info.copy(); } @SuppressWarnings({"unchecked","rawtypes"}) - public ApiInfo(PluginMeta info, List errs) { - this.info = info; + public ApiInfo(PluginMetaHolder infoHolder, List errs) { + this.holder = infoHolder; + this.info = infoHolder.meta; PluginInfo.ClassName klassInfo = new PluginInfo.ClassName(info.klass); pkg = klassInfo.pkg; if (pkg != null) { @@ -349,7 +379,7 @@ public class ContainerPluginsRegistry implements ClusterPropertiesListener, MapW } } - @SuppressWarnings({"rawtypes"}) + @SuppressWarnings({"rawtypes", "unchecked"}) public void init() throws Exception { if (this.holders != null) return; Constructor constructor = klas.getConstructors()[0]; @@ -360,6 +390,13 @@ public class ContainerPluginsRegistry implements ClusterPropertiesListener, MapW } else { throw new RuntimeException("Must have a no-arg constructor or CoreContainer constructor "); } + if (instance instanceof ConfigurablePlugin) { + Class c = getConfigClass((ConfigurablePlugin) instance); + if (c != null) { + MapWriter initVal = mapper.readValue(Utils.toJSON(holder.original), c); + ((ConfigurablePlugin) instance).configure(initVal); + } + } if (instance instanceof ResourceLoaderAware) { try { ((ResourceLoaderAware) instance).inform(pkgVersion.getLoader()); @@ -372,9 +409,34 @@ public class ContainerPluginsRegistry implements ClusterPropertiesListener, MapW holders.add(new ApiHolder((AnnotatedApi) api)); } } + } - public ApiInfo createInfo(PluginMeta info, List errs) { + /**Get the generic type of a {@link ConfigurablePlugin} + */ + @SuppressWarnings("rawtypes") + public static Class getConfigClass(ConfigurablePlugin o) { + Class klas = o.getClass(); + do { + Type[] interfaces = klas.getGenericInterfaces(); + for (Type type : interfaces) { + if (type instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) type; + if (parameterizedType.getRawType() == ConfigurablePlugin.class) { + return (Class) parameterizedType.getActualTypeArguments()[0]; + } + } + } + klas = klas.getSuperclass(); + } while (klas != null && klas != Object.class); + return null; + } + + public ApiInfo createInfo(Map info, List errs) throws IOException { + return new ApiInfo(new PluginMetaHolder(info), errs); + + } + public ApiInfo createInfo(PluginMetaHolder info, List errs) { return new ApiInfo(info, errs); } diff --git a/solr/core/src/java/org/apache/solr/api/V2HttpCall.java b/solr/core/src/java/org/apache/solr/api/V2HttpCall.java index 5eae502e9e9..504db1fbfa4 100644 --- a/solr/core/src/java/org/apache/solr/api/V2HttpCall.java +++ b/solr/core/src/java/org/apache/solr/api/V2HttpCall.java @@ -136,7 +136,7 @@ public class V2HttpCall extends HttpSolrCall { initAdminRequest(path); return; } else { - throw new SolrException(SolrException.ErrorCode.NOT_FOUND, "no core retrieved for " + origCorename); + throw new SolrException(SolrException.ErrorCode.NOT_FOUND, "no core retrieved for core name: " + origCorename + ". Path : "+ path); } } diff --git a/solr/core/src/java/org/apache/solr/handler/admin/ContainerPluginsApi.java b/solr/core/src/java/org/apache/solr/handler/admin/ContainerPluginsApi.java index aff548409c1..57bcbef1ba9 100644 --- a/solr/core/src/java/org/apache/solr/handler/admin/ContainerPluginsApi.java +++ b/solr/core/src/java/org/apache/solr/handler/admin/ContainerPluginsApi.java @@ -19,10 +19,7 @@ package org.apache.solr.handler.admin; import java.io.IOException; import java.lang.invoke.MethodHandles; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.function.Function; import java.util.function.Supplier; @@ -94,7 +91,7 @@ public class ContainerPluginsApi { payload.addError(info.name + " already exists"); return null; } - map.put(info.name, info); + map.put(info.name, payload.getDataMap()); return map; }); } @@ -122,14 +119,19 @@ public class ContainerPluginsApi { payload.addError("No such plugin: " + info.name); return null; } else { - map.put(info.name, info); + Map jsonObj = payload.getDataMap(); + if(Objects.equals(jsonObj, existing)) { + //no need to change anything + return null; + } + map.put(info.name, jsonObj); return map; } }); } } - private void validateConfig(PayloadObj payload, PluginMeta info) { + private void validateConfig(PayloadObj payload, PluginMeta info) throws IOException { if (info.klass.indexOf(':') > 0) { if (info.version == null) { payload.addError("Using package. must provide a packageVersion"); @@ -137,7 +139,7 @@ public class ContainerPluginsApi { } } List errs = new ArrayList<>(); - ContainerPluginsRegistry.ApiInfo apiInfo = coreContainer.getContainerPluginsRegistry().createInfo(info, errs); + ContainerPluginsRegistry.ApiInfo apiInfo = coreContainer.getContainerPluginsRegistry().createInfo( payload.getDataMap(), errs); if (!errs.isEmpty()) { for (String err : errs) payload.addError(err); return; diff --git a/solr/core/src/test/org/apache/solr/handler/TestContainerPlugin.java b/solr/core/src/test/org/apache/solr/handler/TestContainerPlugin.java index 3465a5bc8aa..01ab39f3162 100644 --- a/solr/core/src/test/org/apache/solr/handler/TestContainerPlugin.java +++ b/solr/core/src/test/org/apache/solr/handler/TestContainerPlugin.java @@ -28,6 +28,8 @@ import org.apache.commons.io.IOUtils; import org.apache.lucene.util.ResourceLoader; import org.apache.lucene.util.ResourceLoaderAware; import org.apache.solr.api.Command; +import org.apache.solr.api.ConfigurablePlugin; +import org.apache.solr.api.ContainerPluginsRegistry; import org.apache.solr.api.EndPoint; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.SolrServerException; @@ -41,6 +43,8 @@ import org.apache.solr.cloud.ClusterSingleton; import org.apache.solr.cloud.MiniSolrCloudCluster; import org.apache.solr.cloud.SolrCloudTestCase; import org.apache.solr.common.NavigableObject; +import org.apache.solr.common.annotation.JsonProperty; +import org.apache.solr.common.util.ReflectMapWriter; import org.apache.solr.common.util.Utils; import org.apache.solr.core.CoreContainer; import org.apache.solr.core.SolrResourceLoader; @@ -58,7 +62,6 @@ import org.junit.Test; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; import static org.apache.solr.client.solrj.SolrRequest.METHOD.GET; -import static org.apache.solr.client.solrj.SolrRequest.METHOD.POST; import static org.apache.solr.filestore.TestDistribPackageStore.readFile; import static org.apache.solr.filestore.TestDistribPackageStore.uploadKey; @@ -88,7 +91,7 @@ public class TestContainerPlugin extends SolrCloudTestCase { //test with an invalid class V2Request req = new V2Request.Builder("/cluster/plugin") .forceV2(true) - .withMethod(POST) + .POST() .withPayload(singletonMap("add", plugin)) .build(); expectError(req, cluster.getSolrClient(), errPath, "No method with @Command in class"); @@ -100,7 +103,7 @@ public class TestContainerPlugin extends SolrCloudTestCase { //just check if the plugin is indeed registered V2Request readPluginState = new V2Request.Builder("/cluster/plugin") .forceV2(true) - .withMethod(GET) + .GET() .build(); V2Response rsp = readPluginState.process(cluster.getSolrClient()); assertEquals(C3.class.getName(), rsp._getStr("/plugin/testplugin/class", null)); @@ -109,13 +112,13 @@ public class TestContainerPlugin extends SolrCloudTestCase { TestDistribPackageStore.assertResponseValues(10, () -> new V2Request.Builder("/plugin/my/plugin") .forceV2(true) - .withMethod(GET) + .GET() .build().process(cluster.getSolrClient()), ImmutableMap.of("/testkey", "testval")); //now remove the plugin new V2Request.Builder("/cluster/plugin") - .withMethod(POST) + .POST() .forceV2(true) .withPayload("{remove : testplugin}") .build() @@ -140,19 +143,19 @@ public class TestContainerPlugin extends SolrCloudTestCase { TestDistribPackageStore.assertResponseValues(10, () -> new V2Request.Builder("/my-random-name/my/plugin") .forceV2(true) - .withMethod(GET) + .GET() .build().process(cluster.getSolrClient()), ImmutableMap.of("/method.name", "m1")); TestDistribPackageStore.assertResponseValues(10, () -> new V2Request.Builder("/my-random-prefix/their/plugin") .forceV2(true) - .withMethod(GET) + .GET() .build().process(cluster.getSolrClient()), ImmutableMap.of("/method.name", "m2")); //now remove the plugin new V2Request.Builder("/cluster/plugin") - .withMethod(POST) + .POST() .forceV2(true) .withPayload("{remove : my-random-name}") .build() @@ -160,12 +163,12 @@ public class TestContainerPlugin extends SolrCloudTestCase { expectFail( () -> new V2Request.Builder("/my-random-prefix/their/plugin") .forceV2(true) - .withMethod(GET) + .GET() .build() .process(cluster.getSolrClient())); expectFail(() -> new V2Request.Builder("/my-random-prefix/their/plugin") .forceV2(true) - .withMethod(GET) + .GET() .build() .process(cluster.getSolrClient())); @@ -177,7 +180,7 @@ public class TestContainerPlugin extends SolrCloudTestCase { //just check if the plugin is indeed registered readPluginState = new V2Request.Builder("/cluster/plugin") .forceV2(true) - .withMethod(GET) + .GET() .build(); rsp = readPluginState.process(cluster.getSolrClient()); assertEquals(C6.class.getName(), rsp._getStr("/plugin/clusterSingleton/class", null)); @@ -185,7 +188,47 @@ public class TestContainerPlugin extends SolrCloudTestCase { assertTrue("ccProvided", C6.ccProvided); assertTrue("startCalled", C6.startCalled); assertFalse("stopCalled", C6.stopCalled); - // kill the Overseer leader + + assertEquals( CConfig.class, ContainerPluginsRegistry.getConfigClass(new CC())); + assertEquals( CConfig.class, ContainerPluginsRegistry.getConfigClass(new CC1())); + assertEquals( CConfig.class, ContainerPluginsRegistry.getConfigClass(new CC2())); + + CConfig p = new CConfig(); + p.boolVal = Boolean.TRUE; + p.strVal = "Something"; + p.longVal = 1234L; + p.name = "hello"; + p.klass = CC.class.getName(); + + new V2Request.Builder("/cluster/plugin") + .forceV2(true) + .POST() + .withPayload(singletonMap("add", p)) + .build() + .process(cluster.getSolrClient()); + TestDistribPackageStore.assertResponseValues(10, + () -> new V2Request.Builder("hello/plugin") + .forceV2(true) + .GET() + .build().process(cluster.getSolrClient()), + ImmutableMap.of("/config/boolVal", "true", "/config/strVal", "Something","/config/longVal", "1234" )); + + p.strVal = "Something else"; + new V2Request.Builder("/cluster/plugin") + .forceV2(true) + .POST() + .withPayload(singletonMap("update", p)) + .build() + .process(cluster.getSolrClient()); + + TestDistribPackageStore.assertResponseValues(10, + () -> new V2Request.Builder("hello/plugin") + .forceV2(true) + .GET() + .build().process(cluster.getSolrClient()), + ImmutableMap.of("/config/boolVal", "true", "/config/strVal", p.strVal,"/config/longVal", "1234" )); + + // kill the Overseer leader for (JettySolrRunner jetty : cluster.getJettySolrRunners()) { if (!jetty.getCoreContainer().getZkController().getOverseer().isClosed()) { cluster.stopJettySolrRunner(jetty); @@ -234,7 +277,7 @@ public class TestContainerPlugin extends SolrCloudTestCase { add.files = singletonList(FILE1); V2Request addPkgVersionReq = new V2Request.Builder("/cluster/package") .forceV2(true) - .withMethod(POST) + .POST() .withPayload(singletonMap("add", add)) .build(); addPkgVersionReq.process(cluster.getSolrClient()); @@ -251,14 +294,14 @@ public class TestContainerPlugin extends SolrCloudTestCase { plugin.version = add.version; final V2Request req1 = new V2Request.Builder("/cluster/plugin") .forceV2(true) - .withMethod(POST) + .POST() .withPayload(singletonMap("add", plugin)) .build(); req1.process(cluster.getSolrClient()); //verify the plugin creation TestDistribPackageStore.assertResponseValues(10, () -> new V2Request.Builder("/cluster/plugin"). - withMethod(GET) + GET() .build().process(cluster.getSolrClient()), ImmutableMap.of( "/plugin/myplugin/class", plugin.klass, @@ -267,7 +310,7 @@ public class TestContainerPlugin extends SolrCloudTestCase { //let's test this now Callable invokePlugin = () -> new V2Request.Builder("/plugin/my/path") .forceV2(true) - .withMethod(GET) + .GET() .build().process(cluster.getSolrClient()); TestDistribPackageStore.assertResponseValues(10, invokePlugin, @@ -282,7 +325,7 @@ public class TestContainerPlugin extends SolrCloudTestCase { plugin.version = add.version; new V2Request.Builder("/cluster/plugin") .forceV2(true) - .withMethod(POST) + .POST() .withPayload(singletonMap("update", plugin)) .build() .process(cluster.getSolrClient()); @@ -290,7 +333,7 @@ public class TestContainerPlugin extends SolrCloudTestCase { //now verify if it is indeed updated TestDistribPackageStore.assertResponseValues(10, () -> new V2Request.Builder("/cluster/plugin"). - withMethod(GET) + GET() .build().process(cluster.getSolrClient()), ImmutableMap.of( "/plugin/myplugin/class", plugin.klass, @@ -312,6 +355,50 @@ public class TestContainerPlugin extends SolrCloudTestCase { } } + public static class CC1 extends CC { + + } + public static class CC2 extends CC1 { + + } + public static class CC implements ConfigurablePlugin { + private CConfig cfg; + + + + @Override + public void configure(CConfig cfg) { + this.cfg = cfg; + + } + + @EndPoint(method = GET, + path = "/hello/plugin", + permission = PermissionNameProvider.Name.READ_PERM) + public void m2(SolrQueryRequest req, SolrQueryResponse rsp) { + rsp.add("config", cfg); + } + + } + + public static class CConfig implements ReflectMapWriter { + + @JsonProperty + public String strVal; + + @JsonProperty + public Long longVal; + + @JsonProperty + public Boolean boolVal; + + @JsonProperty + public String name; + + @JsonProperty(value = "class", required = true) + public String klass; + } + public static class C6 implements ClusterSingleton { static boolean startCalled = false; static boolean stopCalled = false; @@ -356,7 +443,6 @@ public class TestContainerPlugin extends SolrCloudTestCase { private SolrResourceLoader resourceLoader; @Override - @SuppressWarnings("unchecked") public void inform(ResourceLoader loader) throws IOException { this.resourceLoader = (SolrResourceLoader) loader; try { diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/request/V2Request.java b/solr/solrj/src/java/org/apache/solr/client/solrj/request/V2Request.java index 932eb6bdd5b..fda9ed337d6 100644 --- a/solr/solrj/src/java/org/apache/solr/client/solrj/request/V2Request.java +++ b/solr/solrj/src/java/org/apache/solr/client/solrj/request/V2Request.java @@ -165,6 +165,26 @@ public class V2Request extends SolrRequest implements MapWriter { return this; } + public Builder POST() { + this.method = METHOD.POST; + return this; + } + + public Builder GET() { + this.method = METHOD.GET; + return this; + } + + public Builder PUT() { + this.method = METHOD.PUT; + return this; + } + + public Builder DELETE() { + this.method = METHOD.DELETE; + return this; + } + /** * Only for testing. It's always true otherwise */ diff --git a/solr/solrj/src/java/org/apache/solr/common/util/JsonSchemaCreator.java b/solr/solrj/src/java/org/apache/solr/common/util/JsonSchemaCreator.java index e6cef30ea7c..bc32d943acc 100644 --- a/solr/solrj/src/java/org/apache/solr/common/util/JsonSchemaCreator.java +++ b/solr/solrj/src/java/org/apache/solr/common/util/JsonSchemaCreator.java @@ -87,6 +87,7 @@ public class JsonSchemaCreator { if(p.required()) required.add(name); } if(!required.isEmpty()) map.put("required", new ArrayList<>(required)); + map.put("additionalProperties", true); } }