All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.cedarsoftware.servlet.ConfigurationProvider.groovy Maven / Gradle / Ivy

There is a newer version: 2.6.0
Show newest version
package com.cedarsoftware.servlet

import com.cedarsoftware.util.CaseInsensitiveMap
import com.cedarsoftware.util.Converter
import com.cedarsoftware.util.ReflectionUtils
import com.cedarsoftware.util.StringUtilities
import com.cedarsoftware.util.io.JsonIoException
import com.cedarsoftware.util.io.JsonObject
import com.cedarsoftware.util.io.JsonReader
import com.cedarsoftware.util.io.MetaUtils
import groovy.transform.CompileStatic
import org.springframework.context.ApplicationContext
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.context.support.WebApplicationContextUtils

import javax.servlet.ServletConfig
import javax.servlet.http.HttpServletRequest
import java.lang.annotation.Annotation
import java.lang.reflect.Field
import java.lang.reflect.Method
import java.util.concurrent.ConcurrentHashMap
import java.util.regex.Matcher
import java.util.regex.Pattern

/**
 * Implement controller provider.  Controllers are named, targetable objects that
 * clients can invoke methods upon.
 * *
 * @author John DeRegnaucourt ([email protected])
 *         
* Copyright (c) Cedar Software LLC *

* Licensed 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. */ @CompileStatic class ConfigurationProvider { private final ApplicationContext springAppCtx private static final Map methodMap = new ConcurrentHashMap<>() private final ServletConfig servletConfig private static final Pattern cmdUrlPattern = ~'^/([^/]+)/([^/]+)(.*)$' // Allows for /controller/method/blah blah (where anything after method is ignored up to ?) private static final Pattern cmdUrlPattern2 = ~'^/[^/]+/([^/]+)/([^/]+)(.*)$' // Allows for /context/controller/method/blah blah (where anything after method is ignored up to ?) ConfigurationProvider(ServletConfig servletConfig) { this.servletConfig = servletConfig springAppCtx = WebApplicationContextUtils.getWebApplicationContext(servletConfig.servletContext) } ServletConfig getServletConfig() { return servletConfig } /** * Fetch the controller with the given name. * @param name String name of a Controller instance (Spring bean name). * @return Controller instance if successful, otherwise throw BeansException if controller * bean doesn't exist or IllegalArgumentException if class isn't annotated as a controller */ protected Object getController(String name) { if (!springAppCtx.containsBean(name)) { throw new IllegalArgumentException("Attempted to call controller with name: ${name}, but no controller with that name exists") } Object controller = springAppCtx.getBean(name) Class targetType = controller.class Annotation annotation = ReflectionUtils.getClassAnnotation(targetType, RestController.class) if (annotation == null) { throw new IllegalArgumentException("error: target '${controller}' is not marked as a @RestController") } return controller } /** * Get a regex Matcher that matches the URL String for /context/cmd/controller/method * @param request HttpServletRequest passed to the command servlet. * @param json String arguments in JSON form from HTTP request * @return Matcher that pattern matches the URL or */ static Matcher getUrlMatcher(HttpServletRequest request) { Matcher matcher if (StringUtilities.hasContent(request.pathInfo)) { matcher = cmdUrlPattern.matcher(request.pathInfo) } else { String path = request.requestURI - request.contextPath matcher = cmdUrlPattern2.matcher(path) } if (matcher.find() && matcher.groupCount() < 2) { return null } return matcher } /** * Read the JSON request (susceptible to Exceptions that are allowed to be thrown from here), * and then call the appropriate Controller method. The controller method exceptions are * caught and returned carefully as JSON error String responses. Note, this should not happen - if * they do, it is a case of a missing try/catch handler in a Controller method (or Advice around * the Controller). */ Object callController(Object controller, HttpServletRequest request, String json) { final Matcher matcher = getUrlMatcher(request) if (matcher == null) { throw new IllegalArgumentException("error: Invalid JSON request - /controller/method not specified") } // Step 1: Fetch controller instance by name final String controllerName = matcher.group(1) // Step 2: Convert JSON arguments from URL (GET argument or POST body) to Object[] final String methodName = matcher.group(2) Object jArgs = getArguments(json, controllerName, methodName) final Object[] args = (Object[]) jArgs // Step 3: Find and invoke method // Wrap the call to the Controller so we can detect any methods that fail to catch exceptions and properly // return them as errors. This separates the errors related to communication from errors related to the // Controller throwing an exception. final Method method = getMethod(controller, controllerName, methodName, args.length) Object result = method.invoke(controller, convertArgs(method, args)) return result } /** * Build the argument list from the passed in json * @param json String argument lists * @param controllerName String name of controller * @param methodName String name of method to call on the controller * @return Object[] of arguments to be passed to method, or Envelope if an error occurred. */ static Object getArguments(String json, String controllerName, String methodName) { Object jArgs try { jArgs = JsonReader.jsonToJava(json) } catch(JsonIoException e) { throw new IllegalArgumentException("error: unable to parse JSON argument list on call '${controllerName}.${methodName}', parse error: ${e.message}") } if (jArgs != null && !(jArgs instanceof Object[])) { throw new IllegalArgumentException("error: Arguments must be either null or a JSON array") } return jArgs } /** * Fetch the named method from the controller. First a local cache will be checked, and if not * found, the method will be found reflectively on the controller. If the method is found, then * it will be checked for a ControllerMethod annotation, which can indicate that it is NOT allowed * to be called. This permits a public controller method to be blocked from remote access. * @param controller Object on which the named method will be found. * @param controllerName String name of the controller (Spring name, n-cube name, etc.) * @param methodName String name of method to be located on the controller. * @param argCount int number of arguments. This is used as part of the cache key to allow for * duplicate method names as long as the argument list length is different. */ protected static Method getMethod(Object controller, String controllerName, String methodName, int argCount) { String methodKey = "${controllerName}.${methodName}.${argCount}" Method method = methodMap[methodKey] if (method == null) { method = getMethod(controller.class, methodName, argCount) if (method == null) { throw new IllegalArgumentException("error: Method not found: ${methodKey}") } Annotation a = ReflectionUtils.getMethodAnnotation(method, ControllerMethod.class) if (a != null) { ControllerMethod cm = (ControllerMethod)a if ("false".equalsIgnoreCase(cm.allow())) { throw new IllegalArgumentException("error: Method '${methodName}' is not allowed to be called via HTTP Request") } } methodMap[methodKey] = method } return method } /** * Reflectively find the requested method on the requested class. * @param c Class containing the method * @param name String method name * @param argc int number of arguments * @return Method instance located on the passed in class. */ protected static Method getMethod(Class c, String name, int argc) { Method[] methods = c.methods for (Method method : methods) { if (name == method.name && method.parameterTypes.length == argc) { return method } } return null } /** * Convert the passed in arguments to match the arguments of the passed in method. * @param method Method which contains argument types. * @param args Object[] of values, which need to be converted. * @return Object[] of converted values. The original values from args[], will be * converted to match the argument types in the method. Java-util's Converter.convert() * handles the primitive and simple types (date, etc.). Collections and Arrays will * be converted to match the respective argument type, and Maps will be converted to * classes (if the matching argument is not a Map). This is done by bring the Class * type of the argument into the json-io JsonObject which represents the instance of * the class. */ protected static Object[] convertArgs(Method method, Object[] args) { Object[] converted = new Object[args.length] Class[] types = method.parameterTypes for (int i=0; i < args.length; i++) { if (args[i] == null) { converted[i] = null } else if (args[i] instanceof Class) { // Special handle an argument of type 'Class' because isLogicalPrimitive() is true for Class. converted[i] = args[i] } else if (MetaUtils.isLogicalPrimitive(args[i].class)) { // Marshal all primitive types, including Date, String (any combination of directions - // String to number, number to String, Date to String, etc.) See Converter.convert(). converted[i] = Converter.convert(args[i], types[i]) } else if (args[i] instanceof Collection || args[i].class.array) { // Arrays, Collections (Sets), ... converted[i] = args[i].asType(types[i]) } else if (args[i] instanceof JsonObject) { JsonObject jsonObj = (JsonObject) args[i] Object type = jsonObj["@type"] if (!(type instanceof String) || !StringUtilities.hasContent((String) type)) { jsonObj["@type"] = types[i].name } CmdReader reader = new CmdReader() try { converted[i] = reader.convertParsedMapsToJava(jsonObj) } catch (Exception e) { throw new IllegalArgumentException("Unable to convert JSON object to arg type: ${types[i].name}", e) } } else if (args[i] instanceof Map) { Map map = (Map) args[i] if (Map.class.isAssignableFrom(types[i])) { converted[i] = map } else { // Map on input, being marshalled into a non-map type. Make sure @type gets set correctly. CmdReader reader = new CmdReader() try { JsonObject jsonObject = new JsonObject() jsonObject.putAll(args[i] as Map) jsonObject.type = types[i].name converted[i] = reader.convertParsedMapsToJava(jsonObject) } catch (Exception e) { throw new IllegalArgumentException("Unable to convert Map to arg type: ${types[i].name}", e) } } } else { if (Map.class.isAssignableFrom(types[i])) { converted[i] = objectToMap(args[i]) } else { converted[i] = args[i] } } } return converted } /** * Convert an Object to a Map. This allows an object to then be passed into n-cube as a coordinate. Of course * the returned map can have additional key/value pairs added to it after calling this method, but before calling * getCell(). * @param o Object any Java object to bind to an NCube. * @return Map where the fields of the object are the field names from the class, and the associated values are * the values associated to the fields on the object. */ static Map objectToMap(final Object o) { try { final Collection fields = ReflectionUtils.getDeepDeclaredFields(o.class) final Iterator i = fields.iterator() final Map newCoord = new CaseInsensitiveMap<>() while (i.hasNext()) { final Field field = i.next() final String fieldName = field.name final Object fieldValue = field.get(o) if (newCoord.containsKey(fieldName)) { // This can happen if field name is same between parent and child class (dumb, but possible) newCoord[field.declaringClass.name + '.' + fieldName] = fieldValue } else { newCoord[fieldName] = fieldValue } } return newCoord } catch (Exception e) { throw new RuntimeException("Failed to access field of passed in object.", e) } } /** * Extend JsonReader to gain access to the convertParsedMapsToJava() API. */ static class CmdReader extends JsonReader { Object convertParsedMapsToJava(JsonObject root) { return super.convertParsedMapsToJava(root) } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy