/* * 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.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import com.fasterxml.jackson.databind.ObjectMapper; 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.*; import org.apache.solr.request.SolrQueryRequest; import org.apache.solr.response.SolrQueryResponse; import org.apache.solr.security.AuthorizationContext; import org.apache.solr.security.PermissionNameProvider; import org.apache.solr.util.SolrJacksonAnnotationInspector; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This class implements an Api just from an annotated java class * The class must have an annotation {@link EndPoint} * Each method must have an annotation {@link Command} * The methods that implement a command should have the first 2 parameters * {@link SolrQueryRequest} and {@link SolrQueryResponse} or it may optionally * have a third parameter which could be a java class annotated with jackson annotations. * The third parameter is only valid if it is using a json command payload */ 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 :"; private EndPoint endPoint; private final Map<String, Cmd> commands ; 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) { return getApis(obj.getClass(), obj); } public static List<Api> getApis(Class<? extends Object> theClass , Object obj) { Class<?> klas = null; try { klas = MethodHandles.publicLookup().accessClass(theClass); } catch (IllegalAccessException e) { throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Method may be non-public/inaccessible", e); } if (klas.getAnnotation(EndPoint.class) != null) { EndPoint endPoint = klas.getAnnotation(EndPoint.class); List<Method> methods = new ArrayList<>(); Map<String, Cmd> commands = new HashMap<>(); for (Method m : klas.getMethods()) { Command command = m.getAnnotation(Command.class); if (command != null) { methods.add(m); if (commands.containsKey(command.name())) { throw new RuntimeException("Duplicate commands " + command.name()); } commands.put(command.name(), new Cmd(command.name(), obj, m)); } } if (commands.isEmpty()) { 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)); } else { List<Api> apis = new ArrayList<>(); for (Method m : klas.getMethods()) { EndPoint endPoint = m.getAnnotation(EndPoint.class); if (endPoint == null) continue; Cmd cmd = new Cmd("", obj, m); SpecProvider specProvider = readSpec(endPoint, Collections.singletonList(m)); apis.add(new AnnotatedApi(specProvider, endPoint, Collections.singletonMap("", cmd), null)); } if (apis.isEmpty()) { throw new RuntimeException("Invalid Class : " + klas.getName() + " No @EndPoints"); } return apis; } } private AnnotatedApi(SpecProvider specProvider, EndPoint endPoint, Map<String, Cmd> commands, Api fallback) { super(specProvider); this.endPoint = endPoint; this.fallback = fallback; this.commands = commands; this.singletonCommand = commands.get(""); } @Override public Name getPermissionName(AuthorizationContext request) { return endPoint.permission(); } @SuppressWarnings({"unchecked", "rawtypes"}) private static SpecProvider readSpec(EndPoint endPoint, List<Method> m) { return () -> { Map map = new LinkedHashMap(); List<String> methods = new ArrayList<>(); for (SolrRequest.METHOD method : endPoint.method()) { methods.add(method.name()); } map.put("methods", methods); map.put("url", new ValidatingJsonMap(Collections.singletonMap("paths", Arrays.asList(endPoint.path())))); Map<String, Object> cmds = new HashMap<>(); for (Method method : m) { Command command = method.getAnnotation(Command.class); if (command != null && !command.name().isEmpty()) { cmds.put(command.name(), AnnotatedApi.createSchema(method)); } } if (!cmds.isEmpty()) { map.put("commands", cmds); } return new ValidatingJsonMap(map); }; } @Override public void call(SolrQueryRequest req, SolrQueryResponse rsp) { if (singletonCommand != null) { singletonCommand.invoke(req, rsp, null); return; } List<CommandOperation> cmds = req.getCommands(true); boolean allExists = true; for (CommandOperation cmd : cmds) { if (!commands.containsKey(cmd.name)) { cmd.addError("No such command supported: " + cmd.name); allExists = false; } } if (!allExists) { if (fallback != null) { fallback.call(req, rsp); return; } else { throw new ApiBag.ExceptionWithErrObject(SolrException.ErrorCode.BAD_REQUEST, "Error processing commands", CommandOperation.captureErrors(cmds)); } } for (CommandOperation cmd : cmds) { commands.get(cmd.name).invoke(req, rsp, cmd); } @SuppressWarnings({"rawtypes"}) List<Map> errs = CommandOperation.captureErrors(cmds); if (!errs.isEmpty()) { log.error("{}{}", ERR, Utils.toJSONString(errs)); throw new ApiBag.ExceptionWithErrObject(SolrException.ErrorCode.BAD_REQUEST, ERR, errs); } } static class Cmd { final String command; final MethodHandle method; final Object obj; ObjectMapper mapper = SolrJacksonAnnotationInspector.createObjectMapper(); int paramsCount; @SuppressWarnings({"rawtypes"}) Class parameterClass; boolean isWrappedInPayloadObj = false; Cmd(String command, Object obj, Method method) { this.command = command; this.obj = obj; try { this.method = MethodHandles.publicLookup().unreflect(method); } catch (IllegalAccessException e) { throw new RuntimeException("Unable to access method, may be not public or accessible ", e); } Class<?>[] parameterTypes = method.getParameterTypes(); paramsCount = parameterTypes.length; 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); } } @SuppressWarnings("rawtypes") 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 parameterClass = Map.class; return; } Type t1 = typ.getActualTypeArguments()[0]; if (t1 instanceof ParameterizedType) { ParameterizedType parameterizedType = (ParameterizedType) t1; parameterClass = (Class) parameterizedType.getRawType(); } else { parameterClass = (Class) typ.getActualTypeArguments()[0]; } } } else { parameterClass = (Class) t; } } @SuppressWarnings({"unchecked"}) void invoke(SolrQueryRequest req, SolrQueryResponse rsp, CommandOperation cmd) { try { Object o = null; String commandName = null; if(paramsCount == 1) { if(cmd == null) { if(parameterClass != null) { try { ContentStream stream = req.getContentStreams().iterator().next(); o = mapper.readValue(stream.getStream(), parameterClass); } catch (IOException e) { throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "invalid payload", e); } } } else { commandName = cmd.name; o = cmd.getCommandData(); if (o instanceof Map && parameterClass != null && parameterClass != Map.class) { o = mapper.readValue(Utils.toJSONString(o), parameterClass); } } PayloadObj<Object> payloadObj = new PayloadObj<>(commandName, o, o, req, rsp); cmd = payloadObj; method.invoke(obj, payloadObj); checkForErrorInPayload(cmd); } else if (paramsCount == 2) { method.invoke(obj, req, rsp); } else { o = cmd.getCommandData(); if (o instanceof Map && parameterClass != null) { o = mapper.readValue(Utils.toJSONString(o), parameterClass); } if (isWrappedInPayloadObj) { 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); } checkForErrorInPayload(cmd); } } catch (RuntimeException se) { log.error("Error executing command ", se); throw se; } catch (Throwable e) { log.error("Error executing command : ", e); throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, e); } } 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(); 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) { t = typ.getActualTypeArguments()[0]; } } return JsonSchemaCreator.getSchema(t); } return null; } }