You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by no...@apache.org on 2020/06/25 03:35:03 UTC
[lucene-solr] branch branch_8x updated: SOLR-14404 CoreContainer
level custom requesthandlers
This is an automated email from the ASF dual-hosted git repository.
noble pushed a commit to branch branch_8x
in repository https://gitbox.apache.org/repos/asf/lucene-solr.git
The following commit(s) were added to refs/heads/branch_8x by this push:
new 8966a1e SOLR-14404 CoreContainer level custom requesthandlers
8966a1e is described below
commit 8966a1e36529debc7331320a69ed1bccc12d3e9a
Author: Noble Paul <no...@users.noreply.github.com>
AuthorDate: Thu Jun 25 13:08:51 2020 +1000
SOLR-14404 CoreContainer level custom requesthandlers
---
solr/CHANGES.txt | 2 +
.../src/java/org/apache/solr/api/AnnotatedApi.java | 113 +++++---
solr/core/src/java/org/apache/solr/api/ApiBag.java | 9 +
.../apache/solr/api/CustomContainerPlugins.java | 313 +++++++++++++++++++++
.../src/java/org/apache/solr/api/PayloadObj.java | 35 ++-
.../src/java/org/apache/solr/api/V2HttpCall.java | 29 +-
.../java/org/apache/solr/core/CoreContainer.java | 24 +-
.../src/java/org/apache/solr/core/PluginInfo.java | 4 +-
.../solr/handler/admin/ContainerPluginsApi.java | 179 ++++++++++++
.../java/org/apache/solr/pkg/PackageLoader.java | 8 +
.../runtimecode/MyPlugin.java} | 28 +-
.../runtimecode/containerplugin.v.1.jar.bin | Bin 0 -> 867 bytes
.../runtimecode/containerplugin.v.2.jar.bin | Bin 0 -> 867 bytes
solr/core/src/test-files/runtimecode/sig.txt | 8 +
.../solr/filestore/TestDistribPackageStore.java | 15 +-
.../apache/solr/handler/TestContainerPlugin.java | 305 ++++++++++++++++++++
.../solr/handler/admin/TestApiFramework.java | 34 +--
.../src/test/org/apache/solr/pkg/TestPackages.java | 2 +-
.../client/solrj/request/beans/PluginMeta.java | 58 ++++
.../java/org/apache/solr/common/util/PathTrie.java | 48 +++-
.../org/apache/solr/common/util/TestPathTrie.java | 8 +
21 files changed, 1110 insertions(+), 112 deletions(-)
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 5625b6b..fbc4425 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -26,6 +26,8 @@ New Features
* SOLR-14470: Add streaming expressions to /export handler. (ab, Joel Bernstein)
+* SOLR-14404: CoreContainer level custom requesthandlers (noble)
+
Improvements
---------------------
* SOLR-14316: Remove unchecked type conversion warning in JavaBinCodec's readMapEntry's equals() method
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 f563472..b3d65d0 100644
--- a/solr/core/src/java/org/apache/solr/api/AnnotatedApi.java
+++ b/solr/core/src/java/org/apache/solr/api/AnnotatedApi.java
@@ -18,6 +18,8 @@
package org.apache.solr.api;
+import java.io.Closeable;
+import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@@ -58,7 +60,7 @@ import org.slf4j.LoggerFactory;
* The third parameter is only valid if it is using a json command payload
*/
-public class AnnotatedApi extends Api implements PermissionNameProvider {
+public class AnnotatedApi extends Api implements PermissionNameProvider , Closeable {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
public static final String ERR = "Error executing commands :";
@@ -67,11 +69,29 @@ public class AnnotatedApi extends Api implements PermissionNameProvider {
private final Cmd singletonCommand;
private final Api fallback;
+ @Override
+ public void close() throws IOException {
+ for (Cmd value : commands.values()) {
+ if (value.obj instanceof Closeable) {
+ ((Closeable) value.obj).close();
+ }
+ break;// all objects are same so close only one
+ }
+
+ }
+
+ public EndPoint getEndPoint() {
+ return endPoint;
+ }
+
public static List<Api> getApis(Object obj) {
- Class<? extends Object> klas = obj.getClass();
+ return getApis(obj.getClass(), obj);
+ }
+ public static List<Api> getApis(Class<? extends Object> klas , Object obj) {
if (!Modifier.isPublic(klas.getModifiers())) {
- throw new RuntimeException(obj.getClass().getName() + " is not public");
+ throw new RuntimeException(klas.getName() + " is not public");
}
+
if (klas.getAnnotation(EndPoint.class) != null) {
EndPoint endPoint = klas.getAnnotation(EndPoint.class);
List<Method> methods = new ArrayList<>();
@@ -87,7 +107,7 @@ public class AnnotatedApi extends Api implements PermissionNameProvider {
}
}
if (commands.isEmpty()) {
- throw new RuntimeException("No method with @Command in class: " + obj.getClass().getName());
+ throw new RuntimeException("No method with @Command in class: " + klas.getName());
}
SpecProvider specProvider = readSpec(endPoint, methods);
return Collections.singletonList(new AnnotatedApi(specProvider, endPoint, commands, null));
@@ -104,8 +124,9 @@ public class AnnotatedApi extends Api implements PermissionNameProvider {
apis.add(new AnnotatedApi(specProvider, endPoint, Collections.singletonMap("", cmd), null));
}
if (apis.isEmpty()) {
- throw new RuntimeException("Invalid Class : " + obj.getClass().getName() + " No @EndPoints");
+ throw new RuntimeException("Invalid Class : " + klas.getName() + " No @EndPoints");
}
+
return apis;
}
}
@@ -207,30 +228,17 @@ public class AnnotatedApi extends Api implements PermissionNameProvider {
this.method = method;
Class<?>[] parameterTypes = method.getParameterTypes();
paramsCount = parameterTypes.length;
- if (parameterTypes[0] != SolrQueryRequest.class || parameterTypes[1] != SolrQueryResponse.class) {
- throw new RuntimeException("Invalid params for method " + method);
- }
- if (parameterTypes.length == 3) {
- Type t = method.getGenericParameterTypes()[2];
- if (t instanceof ParameterizedType) {
- ParameterizedType typ = (ParameterizedType) t;
- if (typ.getRawType() == PayloadObj.class) {
- isWrappedInPayloadObj = true;
- Type t1 = typ.getActualTypeArguments()[0];
- if (t1 instanceof ParameterizedType) {
- ParameterizedType parameterizedType = (ParameterizedType) t1;
- c = (Class) parameterizedType.getRawType();
- } else {
- c = (Class) typ.getActualTypeArguments()[0];
- }
- }
- } else {
- c = (Class) t;
+ if (parameterTypes.length == 1) {
+ readPayloadType(method.getGenericParameterTypes()[0]);
+ } else if (parameterTypes.length == 3) {
+ if (parameterTypes[0] != SolrQueryRequest.class || parameterTypes[1] != SolrQueryResponse.class) {
+ throw new RuntimeException("Invalid params for method " + method);
}
+ Type t = method.getGenericParameterTypes()[2];
+ readPayloadType(t);
}
if (parameterTypes.length > 3) {
throw new RuntimeException("Invalid params count for method " + method);
-
}
} else {
throw new RuntimeException(method.toString() + " is not a public static method");
@@ -238,10 +246,43 @@ public class AnnotatedApi extends Api implements PermissionNameProvider {
}
+ private void readPayloadType(Type t) {
+ if (t instanceof ParameterizedType) {
+ ParameterizedType typ = (ParameterizedType) t;
+ if (typ.getRawType() == PayloadObj.class) {
+ isWrappedInPayloadObj = true;
+ if(typ.getActualTypeArguments().length == 0){
+ //this is a raw type
+ c = Map.class;
+ return;
+ }
+ Type t1 = typ.getActualTypeArguments()[0];
+ if (t1 instanceof ParameterizedType) {
+ ParameterizedType parameterizedType = (ParameterizedType) t1;
+ c = (Class) parameterizedType.getRawType();
+ } else {
+ c = (Class) typ.getActualTypeArguments()[0];
+ }
+ }
+ } else {
+ c = (Class) t;
+ }
+ }
+
+
@SuppressWarnings({"unchecked"})
void invoke(SolrQueryRequest req, SolrQueryResponse rsp, CommandOperation cmd) {
try {
- if (paramsCount == 2) {
+ if(paramsCount ==1) {
+ Object o = cmd.getCommandData();
+ if (o instanceof Map && c != null && c != Map.class) {
+ o = mapper.readValue(Utils.toJSONString(o), c);
+ }
+ PayloadObj<Object> payloadObj = new PayloadObj<>(cmd.name, cmd.getCommandData(), o, req, rsp);
+ cmd = payloadObj;
+ method.invoke(obj, payloadObj);
+ checkForErrorInPayload(cmd);
+ } else if (paramsCount == 2) {
method.invoke(obj, req, rsp);
} else {
Object o = cmd.getCommandData();
@@ -249,16 +290,13 @@ public class AnnotatedApi extends Api implements PermissionNameProvider {
o = mapper.readValue(Utils.toJSONString(o), c);
}
if (isWrappedInPayloadObj) {
- PayloadObj<Object> payloadObj = new PayloadObj<>(cmd.name, cmd.getCommandData(), o);
+ PayloadObj<Object> payloadObj = new PayloadObj<>(cmd.name, cmd.getCommandData(), o, req, rsp);
cmd = payloadObj;
method.invoke(obj, req, rsp, payloadObj);
} else {
method.invoke(obj, req, rsp, o);
}
- if (cmd.hasError()) {
- throw new ApiBag.ExceptionWithErrObject(SolrException.ErrorCode.BAD_REQUEST, "Error executing command",
- CommandOperation.captureErrors(Collections.singletonList(cmd)));
- }
+ checkForErrorInPayload(cmd);
}
@@ -274,12 +312,21 @@ public class AnnotatedApi extends Api implements PermissionNameProvider {
}
}
+
+ private void checkForErrorInPayload(CommandOperation cmd) {
+ if (cmd.hasError()) {
+ throw new ApiBag.ExceptionWithErrObject(SolrException.ErrorCode.BAD_REQUEST, "Error executing command",
+ CommandOperation.captureErrors(Collections.singletonList(cmd)));
+ }
+ }
}
public static Map<String, Object> createSchema(Method m) {
Type[] types = m.getGenericParameterTypes();
- if (types.length == 3) {
- Type t = types[2];
+ Type t = null;
+ if (types.length == 3) t = types[2]; // (SolrQueryRequest req, SolrQueryResponse rsp, PayloadObj<PluginMeta>)
+ if(types.length == 1) t = types[0];// (PayloadObj<PluginMeta>)
+ if (t != null) {
if (t instanceof ParameterizedType) {
ParameterizedType typ = (ParameterizedType) t;
if (typ.getRawType() == PayloadObj.class) {
diff --git a/solr/core/src/java/org/apache/solr/api/ApiBag.java b/solr/core/src/java/org/apache/solr/api/ApiBag.java
index 5740755..8b64829 100644
--- a/solr/core/src/java/org/apache/solr/api/ApiBag.java
+++ b/solr/core/src/java/org/apache/solr/api/ApiBag.java
@@ -31,6 +31,7 @@ import java.util.stream.Collectors;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
+import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SpecProvider;
import org.apache.solr.common.util.CommandOperation;
@@ -150,6 +151,14 @@ public class ApiBag {
registry.insert(copy, substitutes, introspect);
}
+ public synchronized Api unregister(SolrRequest.METHOD method, String path) {
+ List<String> l = PathTrie.getPathSegments(path);
+ List<String> introspectPath = new ArrayList<>(l);
+ introspectPath.add("_introspect");
+ getRegistry(method.toString()).remove(introspectPath);
+ return getRegistry(method.toString()).remove(l);
+ }
+
public static class IntrospectApi extends Api {
Api baseApi;
final boolean isCoreSpecific;
diff --git a/solr/core/src/java/org/apache/solr/api/CustomContainerPlugins.java b/solr/core/src/java/org/apache/solr/api/CustomContainerPlugins.java
new file mode 100644
index 0000000..9246dac
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/api/CustomContainerPlugins.java
@@ -0,0 +1,313 @@
+/*
+ * 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 java.io.Closeable;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.solr.client.solrj.SolrRequest;
+import org.apache.solr.client.solrj.request.beans.PluginMeta;
+import org.apache.solr.common.annotation.JsonProperty;
+import org.apache.solr.common.cloud.ClusterPropertiesListener;
+import org.apache.solr.common.util.Pair;
+import org.apache.solr.common.util.PathTrie;
+import org.apache.solr.common.util.ReflectMapWriter;
+import org.apache.solr.common.util.StrUtils;
+import org.apache.solr.common.util.Utils;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.handler.admin.ContainerPluginsApi;
+import org.apache.solr.pkg.PackageLoader;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.util.SolrJacksonAnnotationInspector;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.apache.lucene.util.IOUtils.closeWhileHandlingException;
+
+public class CustomContainerPlugins implements ClusterPropertiesListener {
+ private final ObjectMapper mapper = SolrJacksonAnnotationInspector.createObjectMapper();
+ private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ final CoreContainer coreContainer;
+ final ApiBag containerApiBag;
+
+ private final Map<String, ApiInfo> currentPlugins = new HashMap<>();
+
+ @Override
+ public boolean onChange(Map<String, Object> properties) {
+ refresh();
+ return false;
+ }
+ public CustomContainerPlugins(CoreContainer coreContainer, ApiBag apiBag) {
+ this.coreContainer = coreContainer;
+ this.containerApiBag = apiBag;
+ }
+
+ public synchronized void refresh() {
+ Map<String, Object> pluginInfos = null;
+ try {
+ pluginInfos = ContainerPluginsApi.plugins(coreContainer.zkClientSupplier);
+ } catch (IOException e) {
+ log.error("Could not read plugins data", e);
+ return;
+ }
+ Map<String,PluginMeta> newState = new HashMap<>(pluginInfos.size());
+ for (Map.Entry<String, Object> e : pluginInfos.entrySet()) {
+ try {
+ newState.put(e.getKey(),
+ mapper.readValue(Utils.toJSON(e.getValue()), PluginMeta.class));
+ } catch (Exception exp) {
+ log.error("Invalid apiInfo configuration :", exp);
+ }
+ }
+
+ Map<String, PluginMeta> currentState = new HashMap<>();
+ for (Map.Entry<String, ApiInfo> e : currentPlugins.entrySet()) {
+ currentState.put(e.getKey(), e.getValue().info);
+ }
+ Map<String, Diff> diff = compareMaps(currentState, newState);
+ if (diff == null) return;//nothing has changed
+ for (Map.Entry<String, Diff> e : diff.entrySet()) {
+ if (e.getValue() == Diff.UNCHANGED) continue;
+ if (e.getValue() == Diff.REMOVED) {
+ ApiInfo apiInfo = currentPlugins.remove(e.getKey());
+ if (apiInfo == null) continue;
+ for (ApiHolder holder : apiInfo.holders) {
+ Api old = containerApiBag.unregister(holder.api.getEndPoint().method()[0], holder.api.getEndPoint().path()[0]);
+ if (old instanceof Closeable) {
+ closeWhileHandlingException((Closeable) old);
+ }
+ }
+ } else {
+ //ADDED or UPDATED
+ PluginMeta info = newState.get(e.getKey());
+ ApiInfo apiInfo = null;
+ List<String> errs = new ArrayList<>();
+ apiInfo = new ApiInfo(info, errs);
+ if (!errs.isEmpty()) {
+ log.error(StrUtils.join(errs, ','));
+ continue;
+ }
+ try {
+ apiInfo.init();
+ } catch (Exception exp) {
+ log.error("Cannot install apiInfo ", exp);
+ continue;
+ }
+ if (e.getValue() == Diff.ADDED) {
+ for (ApiHolder holder : apiInfo.holders) {
+ containerApiBag.register(holder, Collections.singletonMap("plugin-name", e.getKey()));
+ }
+ currentPlugins.put(e.getKey(), apiInfo);
+ } else {
+ ApiInfo old = currentPlugins.put(e.getKey(), apiInfo);
+ List<ApiHolder> replaced = new ArrayList<>();
+ for (ApiHolder holder : apiInfo.holders) {
+ Api oldApi = containerApiBag.lookup(holder.getPath(),
+ holder.getMethod().toString(), null);
+ if (oldApi instanceof ApiHolder) {
+ replaced.add((ApiHolder) oldApi);
+ }
+ containerApiBag.register(holder, Collections.singletonMap("plugin-name", e.getKey()));
+ }
+ if (old != null) {
+ for (ApiHolder holder : old.holders) {
+ if (replaced.contains(holder)) continue;// this path is present in the new one as well. so it already got replaced
+ containerApiBag.unregister(holder.getMethod(), holder.getPath());
+ }
+ if (old instanceof Closeable) {
+ closeWhileHandlingException((Closeable) old);
+ }
+ }
+ }
+ }
+
+ }
+ }
+
+ private static class ApiHolder extends Api {
+ final AnnotatedApi api;
+
+ protected ApiHolder(AnnotatedApi api) {
+ super(api);
+ this.api = api;
+ }
+
+ @Override
+ public void call(SolrQueryRequest req, SolrQueryResponse rsp) {
+ api.call(req, rsp);
+ }
+
+ public String getPath(){
+ return api.getEndPoint().path()[0];
+ }
+
+ public SolrRequest.METHOD getMethod(){
+ return api.getEndPoint().method()[0];
+
+ }
+ }
+
+ @SuppressWarnings({"rawtypes"})
+ public class ApiInfo implements ReflectMapWriter {
+ List<ApiHolder> holders;
+ @JsonProperty
+ private final PluginMeta info;
+
+ @JsonProperty(value = "package")
+ public final String pkg;
+
+ private PackageLoader.Package.Version pkgVersion;
+ private Class klas;
+ Object instance;
+
+
+ @SuppressWarnings({"unchecked","rawtypes"})
+ public ApiInfo(PluginMeta info, List<String> errs) {
+ this.info = info;
+ Pair<String, String> klassInfo = org.apache.solr.core.PluginInfo.parseClassName(info.klass);
+ pkg = klassInfo.first();
+ if (pkg != null) {
+ PackageLoader.Package p = coreContainer.getPackageLoader().getPackage(pkg);
+ if (p == null) {
+ errs.add("Invalid package " + klassInfo.first());
+ return;
+ }
+ this.pkgVersion = p.getVersion(info.version);
+ if (pkgVersion == null) {
+ errs.add("No such package version:" + pkg + ":" + info.version + " . available versions :" + p.allVersions());
+ return;
+ }
+ try {
+ klas = pkgVersion.getLoader().findClass(klassInfo.second(), Object.class);
+ } catch (Exception e) {
+ log.error("Error loading class", e);
+ errs.add("Error loading class " + e.toString());
+ return;
+ }
+ } else {
+ try {
+ klas = Class.forName(klassInfo.second());
+ } catch (ClassNotFoundException e) {
+ errs.add("Error loading class " + e.toString());
+ return;
+ }
+ pkgVersion = null;
+ }
+ if (!Modifier.isPublic(klas.getModifiers())) {
+ errs.add("Class must be public and static : " + klas.getName());
+ return;
+ }
+
+ try {
+ List<Api> apis = AnnotatedApi.getApis(klas, null);
+ for (Object api : apis) {
+ EndPoint endPoint = ((AnnotatedApi) api).getEndPoint();
+ if (endPoint.path().length > 1 || endPoint.method().length > 1) {
+ errs.add("Only one HTTP method and url supported for each API");
+ }
+ if (endPoint.method().length != 1 || endPoint.path().length != 1) {
+ errs.add("The @EndPint must have exactly one method and path attributes");
+ }
+ List<String> pathSegments = StrUtils.splitSmart(endPoint.path()[0], '/', true);
+ PathTrie.replaceTemplates(pathSegments, Collections.singletonMap("plugin-name", info.name));
+ if (V2HttpCall.knownPrefixes.contains(pathSegments.get(0))) {
+ errs.add("path must not have a prefix: "+pathSegments.get(0));
+ }
+
+ }
+ } catch (Exception e) {
+ errs.add(e.toString());
+ }
+ if (!errs.isEmpty()) return;
+
+ Constructor constructor = klas.getConstructors()[0];
+ if (constructor.getParameterTypes().length > 1 ||
+ (constructor.getParameterTypes().length == 1 && constructor.getParameterTypes()[0] != CoreContainer.class)) {
+ errs.add("Must have a no-arg constructor or CoreContainer constructor and it must not be a non static inner class");
+ return;
+ }
+ if (!Modifier.isPublic(constructor.getModifiers())) {
+ errs.add("Must have a public constructor ");
+ return;
+ }
+ }
+
+ @SuppressWarnings({"rawtypes"})
+ public void init() throws Exception {
+ if (this.holders != null) return;
+ Constructor constructor = klas.getConstructors()[0];
+ if (constructor.getParameterTypes().length == 0) {
+ instance = constructor.newInstance();
+ } else if (constructor.getParameterTypes().length == 1 && constructor.getParameterTypes()[0] == CoreContainer.class) {
+ instance = constructor.newInstance(coreContainer);
+ } else {
+ throw new RuntimeException("Must have a no-arg constructor or CoreContainer constructor ");
+ }
+ this.holders = new ArrayList<>();
+ for (Api api : AnnotatedApi.getApis(instance)) {
+ holders.add(new ApiHolder((AnnotatedApi) api));
+ }
+ }
+ }
+
+ public ApiInfo createInfo(PluginMeta info, List<String> errs) {
+ return new ApiInfo(info, errs);
+
+ }
+
+ public enum Diff {
+ ADDED, REMOVED, UNCHANGED, UPDATED;
+ }
+
+ public static Map<String, Diff> compareMaps(Map<String,? extends Object> a, Map<String,? extends Object> b) {
+ if(a.isEmpty() && b.isEmpty()) return null;
+ Map<String, Diff> result = new HashMap<>(Math.max(a.size(), b.size()));
+ a.forEach((k, v) -> {
+ Object newVal = b.get(k);
+ if (newVal == null) {
+ result.put(k, Diff.REMOVED);
+ return;
+ }
+ result.put(k, Objects.equals(v, newVal) ?
+ Diff.UNCHANGED :
+ Diff.UPDATED);
+ });
+
+ b.forEach((k, v) -> {
+ if (a.get(k) == null) result.put(k, Diff.ADDED);
+ });
+
+ for (Diff value : result.values()) {
+ if (value != Diff.UNCHANGED) return result;
+ }
+
+ return null;
+ }
+}
diff --git a/solr/core/src/java/org/apache/solr/api/PayloadObj.java b/solr/core/src/java/org/apache/solr/api/PayloadObj.java
index c09c442..7941304 100644
--- a/solr/core/src/java/org/apache/solr/api/PayloadObj.java
+++ b/solr/core/src/java/org/apache/solr/api/PayloadObj.java
@@ -18,18 +18,35 @@
package org.apache.solr.api;
import org.apache.solr.common.util.CommandOperation;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
-public class PayloadObj<T> extends CommandOperation {
+/**
+ * Holds the deserialized object for each command and also holds request , response objects
+ */
+public class PayloadObj<T> extends CommandOperation {
+
+ //the deserialized object parameter
+ private T obj;
+ final SolrQueryRequest req;
+ final SolrQueryResponse rsp;
- private T obj;
+ public PayloadObj(String operationName, Object metaData, T obj, SolrQueryRequest req, SolrQueryResponse rsp) {
+ super(operationName, metaData);
+ this.obj = obj;
+ this.req = req;
+ this.rsp = rsp;
+ }
+ public T get() {
+ return obj;
+ }
- public PayloadObj(String operationName, Object metaData, T obj) {
- super(operationName, metaData);
- this.obj = obj;
- }
+ public SolrQueryRequest getRequest() {
+ return req;
+ }
- public T get(){
- return obj;
- }
+ public SolrQueryResponse getResponse() {
+ return rsp;
+ }
}
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 c55a08b..5eae502 100644
--- a/solr/core/src/java/org/apache/solr/api/V2HttpCall.java
+++ b/solr/core/src/java/org/apache/solr/api/V2HttpCall.java
@@ -17,18 +17,6 @@
package org.apache.solr.api;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import java.lang.invoke.MethodHandles;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.function.Supplier;
-
import com.google.common.collect.ImmutableSet;
import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.common.SolrException;
@@ -53,11 +41,15 @@ import org.apache.solr.servlet.SolrRequestParsers;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.lang.invoke.MethodHandles;
+import java.util.*;
+import java.util.function.Supplier;
+
import static org.apache.solr.common.cloud.ZkStateReader.COLLECTION_PROP;
import static org.apache.solr.common.util.PathTrie.getPathSegments;
-import static org.apache.solr.servlet.SolrDispatchFilter.Action.ADMIN;
-import static org.apache.solr.servlet.SolrDispatchFilter.Action.PROCESS;
-import static org.apache.solr.servlet.SolrDispatchFilter.Action.REMOTEQUERY;
+import static org.apache.solr.servlet.SolrDispatchFilter.Action.*;
// class that handle the '/v2' path
@SolrThreadSafe
@@ -130,6 +122,13 @@ public class V2HttpCall extends HttpSolrCall {
} else if ("cores".equals(prefix)) {
origCorename = pieces.get(1);
core = cores.getCore(origCorename);
+ } else {
+ api = getApiInfo(cores.getRequestHandlers(), path, req.getMethod(), fullPath, parts);
+ if(api != null) {
+ //custom plugin
+ initAdminRequest(path);
+ return;
+ }
}
if (core == null) {
log.error(">> path: '{}'", path);
diff --git a/solr/core/src/java/org/apache/solr/core/CoreContainer.java b/solr/core/src/java/org/apache/solr/core/CoreContainer.java
index bb13179..601fbaa 100644
--- a/solr/core/src/java/org/apache/solr/core/CoreContainer.java
+++ b/solr/core/src/java/org/apache/solr/core/CoreContainer.java
@@ -40,6 +40,7 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
+import java.util.function.Supplier;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
@@ -52,6 +53,7 @@ import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.store.Directory;
+import org.apache.solr.api.CustomContainerPlugins;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.cloud.SolrCloudManager;
import org.apache.solr.client.solrj.embedded.EmbeddedSolrServer;
@@ -74,6 +76,7 @@ import org.apache.solr.common.SolrException.ErrorCode;
import org.apache.solr.common.cloud.DocCollection;
import org.apache.solr.common.cloud.Replica;
import org.apache.solr.common.cloud.Replica.State;
+import org.apache.solr.common.cloud.SolrZkClient;
import org.apache.solr.common.cloud.ZkStateReader;
import org.apache.solr.common.util.ExecutorUtil;
import org.apache.solr.common.util.IOUtils;
@@ -89,6 +92,7 @@ import org.apache.solr.handler.SnapShooter;
import org.apache.solr.handler.admin.AutoscalingHistoryHandler;
import org.apache.solr.handler.admin.CollectionsHandler;
import org.apache.solr.handler.admin.ConfigSetsHandler;
+import org.apache.solr.handler.admin.ContainerPluginsApi;
import org.apache.solr.handler.admin.CoreAdminHandler;
import org.apache.solr.handler.admin.HealthCheckHandler;
import org.apache.solr.handler.admin.InfoHandler;
@@ -164,6 +168,15 @@ public class CoreContainer {
}
}
+ private volatile PluginBag<SolrRequestHandler> containerHandlers = new PluginBag<>(SolrRequestHandler.class, null);
+
+ /**
+ * Minimize exposure to CoreContainer. Mostly only ZK interface is required
+ */
+ public final Supplier<SolrZkClient> zkClientSupplier = () -> getZkController().getZkClient();
+
+ private final CustomContainerPlugins customContainerPlugins = new CustomContainerPlugins(this, containerHandlers.getApiBag());
+
protected final Map<String, CoreLoadFailure> coreInitFailures = new ConcurrentHashMap<>();
protected volatile CoreAdminHandler coreAdminHandler = null;
@@ -204,8 +217,6 @@ public class CoreContainer {
private final BlobRepository blobRepository = new BlobRepository(this);
- private volatile PluginBag<SolrRequestHandler> containerHandlers = new PluginBag<>(SolrRequestHandler.class, null);
-
private volatile boolean asyncSolrCoreLoad;
protected volatile SecurityConfHandler securityConfHandler;
@@ -862,6 +873,11 @@ public class CoreContainer {
}
if (isZooKeeperAware()) {
+ customContainerPlugins.refresh();
+ getZkController().zkStateReader.registerClusterPropertiesListener(customContainerPlugins);
+ ContainerPluginsApi containerPluginsApi = new ContainerPluginsApi(this);
+ containerHandlers.getApiBag().registerObject(containerPluginsApi.readAPI);
+ containerHandlers.getApiBag().registerObject(containerPluginsApi.editAPI);
zkSys.getZkController().checkOverseerDesignate();
// initialize this handler here when SolrCloudManager is ready
autoScalingHandler = new AutoScalingHandler(getZkController().getSolrCloudManager(), loader);
@@ -2101,6 +2117,10 @@ public class CoreContainer {
return tragicException != null;
}
+ public CustomContainerPlugins getCustomContainerPlugins(){
+ return customContainerPlugins;
+ }
+
static {
ExecutorUtil.addThreadLocalProvider(SolrRequestInfo.getInheritableThreadLocalProvider());
}
diff --git a/solr/core/src/java/org/apache/solr/core/PluginInfo.java b/solr/core/src/java/org/apache/solr/core/PluginInfo.java
index 428d72c..cc6615f 100644
--- a/solr/core/src/java/org/apache/solr/core/PluginInfo.java
+++ b/solr/core/src/java/org/apache/solr/core/PluginInfo.java
@@ -64,9 +64,9 @@ public class PluginInfo implements MapSerializable {
/** class names can be prefixed with package name e.g: my_package:my.pkg.Class
* This checks if it is a package name prefixed classname.
- * the return value has first = package name & second = class name
+ * the return value has first = package name and second = class name
*/
- static Pair<String,String > parseClassName(String name) {
+ public static Pair<String,String > parseClassName(String name) {
String pkgName = null;
String className = name;
if (name != null) {
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
new file mode 100644
index 0000000..21c16e3
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/ContainerPluginsApi.java
@@ -0,0 +1,179 @@
+/*
+ * 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.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.function.Function;
+import java.util.function.Supplier;
+
+import org.apache.solr.api.AnnotatedApi;
+import org.apache.solr.api.Command;
+import org.apache.solr.api.CustomContainerPlugins;
+import org.apache.solr.api.EndPoint;
+import org.apache.solr.api.PayloadObj;
+import org.apache.solr.client.solrj.SolrRequest.METHOD;
+import org.apache.solr.client.solrj.request.beans.PluginMeta;
+import org.apache.solr.common.cloud.SolrZkClient;
+import org.apache.solr.common.cloud.ZkStateReader;
+import org.apache.solr.common.util.Utils;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.security.PermissionNameProvider;
+import org.apache.zookeeper.KeeperException;
+import org.apache.zookeeper.data.Stat;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.apache.lucene.util.IOUtils.closeWhileHandlingException;
+
+
+public class ContainerPluginsApi {
+ private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ public static final String PLUGIN = "plugin";
+ private final Supplier<SolrZkClient> zkClientSupplier;
+ private final CoreContainer coreContainer;
+ public final Read readAPI = new Read();
+ public final Edit editAPI = new Edit();
+
+ public ContainerPluginsApi(CoreContainer coreContainer) {
+ this.zkClientSupplier = coreContainer.zkClientSupplier;
+ this.coreContainer = coreContainer;
+ }
+
+ public class Read {
+ @EndPoint(method = METHOD.GET,
+ path = "/cluster/plugin",
+ permission = PermissionNameProvider.Name.COLL_READ_PERM)
+ public void list(SolrQueryRequest req, SolrQueryResponse rsp) throws IOException {
+ rsp.add(PLUGIN, plugins(zkClientSupplier));
+ }
+ }
+
+ @EndPoint(method = METHOD.POST,
+ path = "/cluster/plugin",
+ permission = PermissionNameProvider.Name.COLL_EDIT_PERM)
+ public class Edit {
+
+ @Command(name = "add")
+ public void add(PayloadObj<PluginMeta> payload) throws IOException {
+ PluginMeta info = payload.get();
+ validateConfig(payload, info);
+ if(payload.hasError()) return;
+ persistPlugins(map -> {
+ if (map.containsKey(info.name)) {
+ payload.addError(info.name + " already exists");
+ return null;
+ }
+ map.put(info.name, info);
+ return map;
+ });
+ }
+
+ @Command(name = "remove")
+ public void remove(PayloadObj<String> payload) throws IOException {
+ persistPlugins(map -> {
+ if (map.remove(payload.get()) == null) {
+ payload.addError("No such plugin: " + payload.get());
+ return null;
+ }
+ return map;
+ });
+ }
+
+ @Command(name = "update")
+ @SuppressWarnings("unchecked")
+ public void update(PayloadObj<PluginMeta> payload) throws IOException {
+ PluginMeta info = payload.get();
+ validateConfig(payload, info);
+ if(payload.hasError()) return;
+ persistPlugins(map -> {
+ Map<String, Object> existing = (Map<String, Object>) map.get(info.name);
+ if (existing == null) {
+ payload.addError("No such plugin: " + info.name);
+ return null;
+ } else {
+ map.put(info.name, info);
+ return map;
+ }
+ });
+ }
+ }
+
+ private void validateConfig(PayloadObj<PluginMeta> payload, PluginMeta info) {
+ if (info.klass.indexOf(':') > 0) {
+ if (info.version == null) {
+ payload.addError("Using package. must provide a packageVersion");
+ return;
+ }
+ }
+ List<String> errs = new ArrayList<>();
+ CustomContainerPlugins.ApiInfo apiInfo = coreContainer.getCustomContainerPlugins().createInfo(info, errs);
+ if (!errs.isEmpty()) {
+ for (String err : errs) payload.addError(err);
+ return;
+ }
+ AnnotatedApi api = null ;
+ try {
+ apiInfo.init();
+ } catch (Exception e) {
+ log.error("Error instantiating plugin ", e);
+ errs.add(e.getMessage());
+ return;
+ } finally {
+ closeWhileHandlingException(api);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ public static Map<String, Object> plugins(Supplier<SolrZkClient> zkClientSupplier) throws IOException {
+ SolrZkClient zkClient = zkClientSupplier.get();
+ try {
+ Map<String, Object> clusterPropsJson = (Map<String, Object>) Utils.fromJSON(zkClient.getData(ZkStateReader.CLUSTER_PROPS, null, new Stat(), true));
+ return (Map<String, Object>) clusterPropsJson.computeIfAbsent(PLUGIN, Utils.NEW_LINKED_HASHMAP_FUN);
+ } catch (KeeperException.NoNodeException e) {
+ return new LinkedHashMap<>();
+ } catch (KeeperException | InterruptedException e) {
+ throw new IOException("Error reading cluster property", SolrZkClient.checkInterrupted(e));
+ }
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ private void persistPlugins(Function<Map<String,Object>, Map<String,Object>> modifier) throws IOException {
+ try {
+ zkClientSupplier.get().atomicUpdate(ZkStateReader.CLUSTER_PROPS, bytes -> {
+ Map rawJson = bytes == null ? new LinkedHashMap() :
+ (Map) Utils.fromJSON(bytes);
+ Map pluginsModified = modifier.apply((Map) rawJson.computeIfAbsent(PLUGIN, Utils.NEW_LINKED_HASHMAP_FUN));
+ if (pluginsModified == null) return null;
+ rawJson.put(PLUGIN, pluginsModified);
+ return Utils.toJSON(rawJson);
+ });
+ } catch (KeeperException | InterruptedException e) {
+ throw new IOException("Error reading cluster property", SolrZkClient.checkInterrupted(e));
+ }
+ }
+
+
+}
diff --git a/solr/core/src/java/org/apache/solr/pkg/PackageLoader.java b/solr/core/src/java/org/apache/solr/pkg/PackageLoader.java
index 8ff12a0..1089288 100644
--- a/solr/core/src/java/org/apache/solr/pkg/PackageLoader.java
+++ b/solr/core/src/java/org/apache/solr/pkg/PackageLoader.java
@@ -158,6 +158,10 @@ public class PackageLoader implements Closeable {
return deleted;
}
+ public Set<String> allVersions() {
+ return myVersions.keySet();
+ }
+
private synchronized void updateVersions(List<PackageAPI.PkgVersion> modified) {
for (PackageAPI.PkgVersion v : modified) {
@@ -210,6 +214,10 @@ public class PackageLoader implements Closeable {
return latest == null ? null : myVersions.get(latest);
}
+ public Version getVersion(String version) {
+ return myVersions.get(version);
+ }
+
public Version getLatest(String lessThan) {
if (lessThan == null) {
return getLatest();
diff --git a/solr/core/src/java/org/apache/solr/api/PayloadObj.java b/solr/core/src/test-files/runtimecode/MyPlugin.java
similarity index 51%
copy from solr/core/src/java/org/apache/solr/api/PayloadObj.java
copy to solr/core/src/test-files/runtimecode/MyPlugin.java
index c09c442..cbaa347 100644
--- a/solr/core/src/java/org/apache/solr/api/PayloadObj.java
+++ b/solr/core/src/test-files/runtimecode/MyPlugin.java
@@ -15,21 +15,29 @@
* limitations under the License.
*/
-package org.apache.solr.api;
+package org.apache.solr.handler;
-import org.apache.solr.common.util.CommandOperation;
+import org.apache.solr.api.Command;
+import org.apache.solr.api.EndPoint;
+import org.apache.solr.client.solrj.SolrRequest.METHOD;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.security.PermissionNameProvider;
-public class PayloadObj<T> extends CommandOperation {
+@EndPoint(path = "/plugin/my/path",
+ method = METHOD.GET,
+ permission = PermissionNameProvider.Name.CONFIG_READ_PERM)
+public class MyPlugin {
- private T obj;
+ private final CoreContainer coreContainer;
-
- public PayloadObj(String operationName, Object metaData, T obj) {
- super(operationName, metaData);
- this.obj = obj;
+ public MyPlugin(CoreContainer coreContainer) {
+ this.coreContainer = coreContainer;
}
- public T get(){
- return obj;
+ @Command
+ public void call(SolrQueryRequest req, SolrQueryResponse rsp){
+ rsp.add("myplugin.version", "2.0");
}
}
diff --git a/solr/core/src/test-files/runtimecode/containerplugin.v.1.jar.bin b/solr/core/src/test-files/runtimecode/containerplugin.v.1.jar.bin
new file mode 100644
index 0000000..f9053c8
Binary files /dev/null and b/solr/core/src/test-files/runtimecode/containerplugin.v.1.jar.bin differ
diff --git a/solr/core/src/test-files/runtimecode/containerplugin.v.2.jar.bin b/solr/core/src/test-files/runtimecode/containerplugin.v.2.jar.bin
new file mode 100644
index 0000000..4d1cd99
Binary files /dev/null and b/solr/core/src/test-files/runtimecode/containerplugin.v.2.jar.bin differ
diff --git a/solr/core/src/test-files/runtimecode/sig.txt b/solr/core/src/test-files/runtimecode/sig.txt
index 74bb942..59beb47 100644
--- a/solr/core/src/test-files/runtimecode/sig.txt
+++ b/solr/core/src/test-files/runtimecode/sig.txt
@@ -69,6 +69,14 @@ openssl dgst -sha1 -sign ../cryptokeys/priv_key512.pem expressible.jar.bin | ope
ZOT11arAiPmPZYOHzqodiNnxO9pRyRozWZEBX8XGjU1/HJptFnZK+DI7eXnUtbNaMcbXE2Ze8hh4M/eGyhY8BQ==
+openssl dgst -sha1 -sign priv_key512.pem containerplugin.v.1.jar.bin | openssl enc -base64 | sed 's/+/%2B/g' | tr -d \\n | sed
+
+pmrmWCDafdNpYle2rueAGnU2J6NYlcAey9mkZYbqh+5RdYo2Ln+llLF9voyRj+DDivK9GV1XdtKvD9rgCxlD7Q==
+
+openssl dgst -sha1 -sign ../cryptokeys/priv_key512.pem containerplugin.v.2.jar.bin | openssl enc -base64 | sed 's/+/%2B/g' | tr -d \\n | sed
+
+StR3DmqaUSL7qjDOeVEiCqE+ouiZAkW99fsL48F9oWG047o7NGgwwZ36iGgzDC3S2tPaFjRAd9Zg4UK7OZLQzg==
+
====================sha512====================
openssl dgst -sha512 runtimelibs.jar.bin
diff --git a/solr/core/src/test/org/apache/solr/filestore/TestDistribPackageStore.java b/solr/core/src/test/org/apache/solr/filestore/TestDistribPackageStore.java
index 71bdd0a..94076e2 100644
--- a/solr/core/src/test/org/apache/solr/filestore/TestDistribPackageStore.java
+++ b/solr/core/src/test/org/apache/solr/filestore/TestDistribPackageStore.java
@@ -19,6 +19,7 @@ package org.apache.solr.filestore;
import java.io.FileInputStream;
import java.io.IOException;
+import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.file.Paths;
import java.util.List;
@@ -53,6 +54,7 @@ import org.junit.Before;
import static org.apache.solr.common.util.Utils.JAVABINCONSUMER;
import static org.apache.solr.core.TestDynamicLoading.getFileContent;
+import static org.hamcrest.CoreMatchers.containsString;
@LogLevel("org.apache.solr.filestore.PackageStoreAPI=DEBUG;org.apache.solr.filestore.DistribPackageStore=DEBUG")
public class TestDistribPackageStore extends SolrCloudTestCase {
@@ -88,7 +90,7 @@ public class TestDistribPackageStore extends SolrCloudTestCase {
);
fail("should have failed because of wrong signature ");
} catch (RemoteExecutionException e) {
- assertTrue(e.getMessage().contains("Signature does not match"));
+ assertThat(e.getMessage(), containsString("Signature does not match"));
}
postFile(cluster.getSolrClient(), getFileContent("runtimecode/runtimelibs.jar.bin"),
@@ -192,11 +194,12 @@ public class TestDistribPackageStore extends SolrCloudTestCase {
}
}
+
@SuppressWarnings({"rawtypes"})
- static class Fetcher implements Callable {
+ public static class Fetcher implements Callable {
String url;
JettySolrRunner jetty;
- Fetcher(String s, JettySolrRunner jettySolrRunner){
+ public Fetcher(String s, JettySolrRunner jettySolrRunner){
this.url = s;
this.jetty = jettySolrRunner;
}
@@ -288,6 +291,12 @@ public class TestDistribPackageStore extends SolrCloudTestCase {
assertEquals(name, rsp.getResponse().get(CommonParams.FILE));
}
+ /**
+ * Read and return the contents of the file-like resource
+ * @param fname the name of the resource to read
+ * @return the bytes of the resource
+ * @throws IOException if there is an I/O error reading the contents
+ */
public static byte[] readFile(String fname) throws IOException {
byte[] buf = null;
try (FileInputStream fis = new FileInputStream(getFile(fname))) {
diff --git a/solr/core/src/test/org/apache/solr/handler/TestContainerPlugin.java b/solr/core/src/test/org/apache/solr/handler/TestContainerPlugin.java
new file mode 100644
index 0000000..da9696c
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/handler/TestContainerPlugin.java
@@ -0,0 +1,305 @@
+/*
+ * 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.handler;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.Callable;
+
+import static java.util.Collections.singletonList;
+import static java.util.Collections.singletonMap;
+
+import com.google.common.collect.ImmutableMap;
+import org.apache.solr.api.Command;
+import org.apache.solr.api.EndPoint;
+import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.client.solrj.embedded.JettySolrRunner;
+import org.apache.solr.client.solrj.impl.BaseHttpSolrClient.RemoteExecutionException;
+import org.apache.solr.client.solrj.request.V2Request;
+import org.apache.solr.client.solrj.request.beans.Package;
+import org.apache.solr.client.solrj.request.beans.PluginMeta;
+import org.apache.solr.client.solrj.response.V2Response;
+import org.apache.solr.cloud.MiniSolrCloudCluster;
+import org.apache.solr.cloud.SolrCloudTestCase;
+import org.apache.solr.common.NavigableObject;
+import org.apache.solr.common.util.Utils;
+import org.apache.solr.filestore.PackageStoreAPI;
+import org.apache.solr.filestore.TestDistribPackageStore;
+import org.apache.solr.filestore.TestDistribPackageStore.Fetcher;
+import org.apache.solr.pkg.TestPackages;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.security.PermissionNameProvider;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+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;
+
+public class TestContainerPlugin extends SolrCloudTestCase {
+
+ @Before
+ public void setup() {
+ System.setProperty("enable.packages", "true");
+ }
+
+ @After
+ public void teardown() {
+ System.clearProperty("enable.packages");
+ }
+
+ @Test
+ public void testApi() throws Exception {
+ MiniSolrCloudCluster cluster =
+ configureCluster(4)
+ .withJettyConfig(jetty -> jetty.enableV2(true))
+ .configure();
+ String errPath = "/error/details[0]/errorMessages[0]";
+ try {
+ PluginMeta plugin = new PluginMeta();
+ plugin.name = "testplugin";
+ plugin.klass = C2.class.getName();
+ //test with an invalid class
+ V2Request req = new V2Request.Builder("/cluster/plugin")
+ .forceV2(true)
+ .withMethod(POST)
+ .withPayload(singletonMap("add", plugin))
+ .build();
+ expectError(req, cluster.getSolrClient(), errPath, "No method with @Command in class");
+
+ //test with an invalid class
+ plugin.klass = C1.class.getName();
+ expectError(req, cluster.getSolrClient(), errPath, "No @EndPoints");
+
+ //test with a valid class. This should succeed now
+ plugin.klass = C3.class.getName();
+ req.process(cluster.getSolrClient());
+
+ //just check if the plugin is indeed registered
+ V2Request readPluginState = new V2Request.Builder("/cluster/plugin")
+ .forceV2(true)
+ .withMethod(GET)
+ .build();
+ V2Response rsp = readPluginState.process(cluster.getSolrClient());
+ assertEquals(C3.class.getName(), rsp._getStr("/plugin/testplugin/class", null));
+
+ //let's test the plugin
+ TestDistribPackageStore.assertResponseValues(10,
+ () -> new V2Request.Builder("/plugin/my/plugin")
+ .forceV2(true)
+ .withMethod(GET)
+ .build().process(cluster.getSolrClient()),
+ ImmutableMap.of("/testkey", "testval"));
+
+ //now remove the plugin
+ new V2Request.Builder("/cluster/plugin")
+ .withMethod(POST)
+ .forceV2(true)
+ .withPayload("{remove : testplugin}")
+ .build()
+ .process(cluster.getSolrClient());
+
+ //verify it is removed
+ rsp = readPluginState.process(cluster.getSolrClient());
+ assertEquals(null, rsp._get("/plugin/testplugin/class", null));
+
+ //test with a class @EndPoint methods. This also uses a template in the path name
+ plugin.klass = C4.class.getName();
+ plugin.name = "collections";
+ expectError(req, cluster.getSolrClient(), errPath, "path must not have a prefix: collections");
+
+ plugin.name = "my-random-name";
+ req.process(cluster.getSolrClient());
+
+ //let's test the plugin
+ TestDistribPackageStore.assertResponseValues(10,
+ () -> new V2Request.Builder("/my-random-name/my/plugin")
+ .forceV2(true)
+ .withMethod(GET)
+ .build().process(cluster.getSolrClient()),
+ ImmutableMap.of("/method.name", "m1"));
+
+ TestDistribPackageStore.assertResponseValues(10,
+ () -> new V2Request.Builder("/my-random-name/their/plugin")
+ .forceV2(true)
+ .withMethod(GET)
+ .build().process(cluster.getSolrClient()),
+ ImmutableMap.of("/method.name", "m2"));
+
+ } finally {
+ cluster.shutdown();
+ }
+ }
+ @Test
+ public void testApiFromPackage() throws Exception {
+ MiniSolrCloudCluster cluster =
+ configureCluster(4)
+ .withJettyConfig(jetty -> jetty.enableV2(true))
+ .configure();
+ String FILE1 = "/myplugin/v1.jar";
+ String FILE2 = "/myplugin/v2.jar";
+
+ String errPath = "/error/details[0]/errorMessages[0]";
+ try {
+ byte[] derFile = readFile("cryptokeys/pub_key512.der");
+ uploadKey(derFile, PackageStoreAPI.KEYS_DIR+"/pub_key512.der", cluster);
+ TestPackages.postFileAndWait(cluster, "runtimecode/containerplugin.v.1.jar.bin", FILE1,
+ "pmrmWCDafdNpYle2rueAGnU2J6NYlcAey9mkZYbqh+5RdYo2Ln+llLF9voyRj+DDivK9GV1XdtKvD9rgCxlD7Q==");
+ TestPackages.postFileAndWait(cluster, "runtimecode/containerplugin.v.2.jar.bin", FILE2,
+ "StR3DmqaUSL7qjDOeVEiCqE+ouiZAkW99fsL48F9oWG047o7NGgwwZ36iGgzDC3S2tPaFjRAd9Zg4UK7OZLQzg==");
+
+ // We have two versions of the plugin in 2 different jar files. they are already uploaded to the package store
+ Package.AddVersion add = new Package.AddVersion();
+ add.version = "1.0";
+ add.pkg = "mypkg";
+ add.files = singletonList(FILE1);
+ V2Request addPkgVersionReq = new V2Request.Builder("/cluster/package")
+ .forceV2(true)
+ .withMethod(POST)
+ .withPayload(singletonMap("add", add))
+ .build();
+ addPkgVersionReq.process(cluster.getSolrClient());
+
+ waitForAllNodesToSync(cluster, "/cluster/package", Utils.makeMap(
+ ":result:packages:mypkg[0]:version", "1.0",
+ ":result:packages:mypkg[0]:files[0]", FILE1
+ ));
+
+ // Now lets create a plugin using v1 jar file
+ PluginMeta plugin = new PluginMeta();
+ plugin.name = "myplugin";
+ plugin.klass = "mypkg:org.apache.solr.handler.MyPlugin";
+ plugin.version = add.version;
+ V2Request req1 = new V2Request.Builder("/cluster/plugin")
+ .forceV2(true)
+ .withMethod(POST)
+ .withPayload(singletonMap("add", plugin))
+ .build();
+ req1.process(cluster.getSolrClient());
+ //verify the plugin creation
+ TestDistribPackageStore.assertResponseValues(10,
+ () -> new V2Request.Builder("/cluster/plugin").
+ withMethod(GET)
+ .build().process(cluster.getSolrClient()),
+ ImmutableMap.of(
+ "/plugin/myplugin/class", plugin.klass,
+ "/plugin/myplugin/version", plugin.version
+ ));
+ //let's test this now
+ Callable<NavigableObject> invokePlugin = () -> new V2Request.Builder("/plugin/my/path")
+ .forceV2(true)
+ .withMethod(GET)
+ .build().process(cluster.getSolrClient());
+ TestDistribPackageStore.assertResponseValues(10,
+ invokePlugin,
+ ImmutableMap.of("/myplugin.version", "1.0"));
+
+ //now let's upload the jar file for version 2.0 of the plugin
+ add.version = "2.0";
+ add.files = singletonList(FILE2);
+ addPkgVersionReq.process(cluster.getSolrClient());
+
+ //here the plugin version is updated
+ plugin.version = add.version;
+ new V2Request.Builder("/cluster/plugin")
+ .forceV2(true)
+ .withMethod(POST)
+ .withPayload(singletonMap("update", plugin))
+ .build()
+ .process(cluster.getSolrClient());
+
+ //now verify if it is indeed updated
+ TestDistribPackageStore.assertResponseValues(10,
+ () -> new V2Request.Builder("/cluster/plugin").
+ withMethod(GET)
+ .build().process(cluster.getSolrClient()),
+ ImmutableMap.of(
+ "/plugin/myplugin/class", plugin.klass,
+ "/plugin/myplugin/version", "2.0"
+ ));
+ // invoke the plugin and test thye output
+ TestDistribPackageStore.assertResponseValues(10,
+ invokePlugin,
+ ImmutableMap.of("/myplugin.version", "2.0"));
+ } finally {
+ cluster.shutdown();
+ }
+ }
+
+ public static class C1 {
+
+ }
+
+ @EndPoint(
+ method = GET,
+ path = "/plugin/my/plugin",
+ permission = PermissionNameProvider.Name.COLL_READ_PERM)
+ public class C2 {
+
+
+ }
+
+ @EndPoint(
+ method = GET,
+ path = "/plugin/my/plugin",
+ permission = PermissionNameProvider.Name.COLL_READ_PERM)
+ public static class C3 {
+ @Command
+ public void read(SolrQueryRequest req, SolrQueryResponse rsp) {
+ rsp.add("testkey", "testval");
+ }
+
+ }
+
+ public static class C4 {
+
+ @EndPoint(method = GET,
+ path = "$plugin-name/my/plugin",
+ permission = PermissionNameProvider.Name.READ_PERM)
+ public void m1(SolrQueryRequest req, SolrQueryResponse rsp) {
+ rsp.add("method.name", "m1");
+ }
+
+ @EndPoint(method = GET,
+ path = "$plugin-name/their/plugin",
+ permission = PermissionNameProvider.Name.READ_PERM)
+ public void m2(SolrQueryRequest req, SolrQueryResponse rsp) {
+ rsp.add("method.name", "m2");
+ }
+
+ }
+
+ @SuppressWarnings("unchecked")
+ public static void waitForAllNodesToSync(MiniSolrCloudCluster cluster, String path, Map<String,Object> expected) throws Exception {
+ for (JettySolrRunner jettySolrRunner : cluster.getJettySolrRunners()) {
+ String baseUrl = jettySolrRunner.getBaseUrl().toString().replace("/solr", "/api");
+ String url = baseUrl + path + "?wt=javabin";
+ TestDistribPackageStore.assertResponseValues(10, new Fetcher(url, jettySolrRunner), expected);
+ }
+ }
+
+ private void expectError(V2Request req, SolrClient client, String errPath, String expectErrorMsg) throws IOException, SolrServerException {
+ RemoteExecutionException e = expectThrows(RemoteExecutionException.class, () -> req.process(client));
+ String msg = e.getMetaData()._getStr(errPath, "");
+ assertTrue(expectErrorMsg, msg.contains(expectErrorMsg));
+ }
+}
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/TestApiFramework.java b/solr/core/src/test/org/apache/solr/handler/admin/TestApiFramework.java
index 3c66c7b..5f0a57a 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/TestApiFramework.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/TestApiFramework.java
@@ -17,36 +17,15 @@
package org.apache.solr.handler.admin;
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
import org.apache.solr.SolrTestCaseJ4;
-import org.apache.solr.api.Api;
-import org.apache.solr.api.ApiBag;
-import org.apache.solr.api.Command;
-import org.apache.solr.api.EndPoint;
-import org.apache.solr.api.V2HttpCall;
+import org.apache.solr.api.*;
import org.apache.solr.api.V2HttpCall.CompositeApi;
import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.common.annotation.JsonProperty;
import org.apache.solr.common.params.MapSolrParams;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.params.SolrParams;
-import org.apache.solr.common.util.CommandOperation;
-import org.apache.solr.common.util.ContentStream;
-import org.apache.solr.common.util.ContentStreamBase;
-import org.apache.solr.common.util.JsonSchemaValidator;
-import org.apache.solr.common.util.PathTrie;
-import org.apache.solr.common.util.StrUtils;
-import org.apache.solr.common.util.Utils;
-import org.apache.solr.common.util.ValidatingJsonMap;
+import org.apache.solr.common.util.*;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.core.PluginBag;
import org.apache.solr.handler.PingRequestHandler;
@@ -59,13 +38,16 @@ import org.apache.solr.request.SolrRequestHandler;
import org.apache.solr.response.SolrQueryResponse;
import org.apache.solr.security.PermissionNameProvider;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.*;
+
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.apache.solr.api.ApiBag.EMPTY_SPEC;
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.common.params.CommonParams.COLLECTIONS_HANDLER_PATH;
-import static org.apache.solr.common.params.CommonParams.CONFIGSETS_HANDLER_PATH;
-import static org.apache.solr.common.params.CommonParams.CORES_HANDLER_PATH;
+import static org.apache.solr.common.params.CommonParams.*;
import static org.apache.solr.common.util.ValidatingJsonMap.NOT_NULL;
public class TestApiFramework extends SolrTestCaseJ4 {
diff --git a/solr/core/src/test/org/apache/solr/pkg/TestPackages.java b/solr/core/src/test/org/apache/solr/pkg/TestPackages.java
index 7714a50..a0539ad 100644
--- a/solr/core/src/test/org/apache/solr/pkg/TestPackages.java
+++ b/solr/core/src/test/org/apache/solr/pkg/TestPackages.java
@@ -635,7 +635,7 @@ public class TestPackages extends SolrCloudTestCase {
}
}
- static void postFileAndWait(MiniSolrCloudCluster cluster, String fname, String path, String sig) throws Exception {
+ public static void postFileAndWait(MiniSolrCloudCluster cluster, String fname, String path, String sig) throws Exception {
ByteBuffer fileContent = getFileContent(fname);
String sha512 = DigestUtils.sha512Hex(fileContent.array());
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/PluginMeta.java b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/PluginMeta.java
new file mode 100644
index 0000000..cb4f0aa
--- /dev/null
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/PluginMeta.java
@@ -0,0 +1,58 @@
+/*
+ * 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.client.solrj.request.beans;
+
+import java.util.Objects;
+
+import org.apache.solr.common.annotation.JsonProperty;
+import org.apache.solr.common.util.ReflectMapWriter;
+
+/**
+ * POJO for a plugin metadata used in container plugins
+ */
+@SuppressWarnings({"overrides"})
+public class PluginMeta implements ReflectMapWriter {
+ @JsonProperty(required = true)
+ public String name;
+
+ @JsonProperty(value = "class", required = true)
+ public String klass;
+
+ @JsonProperty
+ public String version;
+
+
+ public PluginMeta copy() {
+ PluginMeta result = new PluginMeta();
+ result.name = name;
+ result.klass = klass;
+ result.version = version;
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof PluginMeta) {
+ PluginMeta that = (PluginMeta) obj;
+ return Objects.equals(this.name, that.name) &&
+ Objects.equals(this.klass, that.klass) &&
+ Objects.equals(this.version, that.version);
+ }
+ return false;
+ }
+}
diff --git a/solr/solrj/src/java/org/apache/solr/common/util/PathTrie.java b/solr/solrj/src/java/org/apache/solr/common/util/PathTrie.java
index 742c59d..e5a7e73e 100644
--- a/solr/solrj/src/java/org/apache/solr/common/util/PathTrie.java
+++ b/solr/solrj/src/java/org/apache/solr/common/util/PathTrie.java
@@ -32,7 +32,7 @@ import static java.util.Collections.emptyList;
*/
public class PathTrie<T> {
private final Set<String> reserved = new HashSet<>();
- Node root = new Node(emptyList(), null);
+ Node root = new Node(emptyList(), null, null);
public PathTrie() {
}
@@ -52,7 +52,11 @@ public class PathTrie<T> {
root.obj = o;
return;
}
+ replaceTemplates(parts, replacements);
+ root.insert(parts, o);
+ }
+ public static void replaceTemplates(List<String> parts, Map<String, String> replacements) {
for (int i = 0; i < parts.size(); i++) {
String part = parts.get(i);
if (part.charAt(0) == '$') {
@@ -64,8 +68,6 @@ public class PathTrie<T> {
parts.set(i, replacement);
}
}
-
- root.insert(parts, o);
}
// /a/b/c will be returned as ["a","b","c"]
@@ -82,6 +84,22 @@ public class PathTrie<T> {
return parts;
}
+ public T remove(List<String> path) {
+ Node node = root.lookupNode(path, 0, null, null);
+ T result = null;
+ if (node != null) {
+ result = node.obj;
+ node.obj = null;
+ if (node.children == null || node.children.isEmpty()) {
+ if (node.parent != null) {
+ node.parent.children.remove(node.name);
+ }
+ }
+ return result;
+ }
+ return result;
+
+ }
public T lookup(String path, Map<String, String> templateValues) {
return root.lookup(getPathSegments(path), 0, templateValues);
@@ -107,8 +125,10 @@ public class PathTrie<T> {
Map<String, Node> children;
T obj;
String templateName;
+ final Node parent;
- Node(List<String> path, T o) {
+ Node(List<String> path, T o, Node parent) {
+ this.parent = parent;
if (path.isEmpty()) {
obj = o;
return;
@@ -133,7 +153,7 @@ public class PathTrie<T> {
matchedChild = children.get(key);
if (matchedChild == null) {
- children.put(key, matchedChild = new Node(path, o));
+ children.put(key, matchedChild = new Node(path, o, this));
}
if (varName != null) {
if (!matchedChild.templateName.equals(varName)) {
@@ -179,17 +199,23 @@ public class PathTrie<T> {
* @param availableSubPaths If not null , available sub paths will be returned in this set
*/
public T lookup(List<String> pathSegments, int index, Map<String, String> templateVariables, Set<String> availableSubPaths) {
- if (templateName != null) templateVariables.put(templateName, pathSegments.get(index - 1));
+ Node node = lookupNode(pathSegments, index, templateVariables, availableSubPaths);
+ return node == null ? null : node.obj;
+ }
+
+ Node lookupNode(List<String> pathSegments, int index, Map<String, String> templateVariables, Set<String> availableSubPaths) {
+ if (templateName != null && templateVariables != null)
+ templateVariables.put(templateName, pathSegments.get(index - 1));
if (pathSegments.size() < index + 1) {
findAvailableChildren("", availableSubPaths);
if (obj == null) {//this is not a leaf node
Node n = children.get("*");
if (n != null) {
- return n.obj;
+ return n;
}
}
- return obj;
+ return this;
}
String piece = pathSegments.get(index);
if (children == null) {
@@ -204,15 +230,15 @@ public class PathTrie<T> {
for (int i = index; i < pathSegments.size(); i++) {
sb.append("/").append(pathSegments.get(i));
}
- templateVariables.put("*", sb.toString());
- return n.obj;
+ if (templateVariables != null) templateVariables.put("*", sb.toString());
+ return n;
}
}
if (n == null) {
return null;
}
- return n.lookup(pathSegments, index + 1, templateVariables, availableSubPaths);
+ return n.lookupNode(pathSegments, index + 1, templateVariables, availableSubPaths);
}
}
diff --git a/solr/solrj/src/test/org/apache/solr/common/util/TestPathTrie.java b/solr/solrj/src/test/org/apache/solr/common/util/TestPathTrie.java
index 5a56821..2b1ad42 100644
--- a/solr/solrj/src/test/org/apache/solr/common/util/TestPathTrie.java
+++ b/solr/solrj/src/test/org/apache/solr/common/util/TestPathTrie.java
@@ -71,5 +71,13 @@ public class TestPathTrie extends SolrTestCaseJ4 {
templateValues.clear();
assertEquals("W" ,pathTrie.lookup("/aa/bb/somepart/tt/hello/world/from/solr", templateValues));
assertEquals(templateValues.get("*"), "/hello/world/from/solr");
+
+ pathTrie.insert("/1/2/{x}/4", emptyMap(), "a");
+ assertEquals("a", pathTrie.lookup("/1/2/3/4", null));
+ templateValues.clear();
+ assertEquals("a", pathTrie.lookup("/1/2/3/4", templateValues));
+ assertEquals(templateValues.get("x"), "3");
+ pathTrie.remove(PathTrie.getPathSegments("/1/2/3/4"));
+ assertEquals(null, pathTrie.lookup("/1/2/3/4", null));
}
}