/**
* Copyright (C) 2011 JTalks.org Team
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.jtalks.jcommune.plugin.api.web;
import com.google.common.annotations.VisibleForTesting;
import org.jtalks.jcommune.plugin.api.PluginLoader;
import org.jtalks.jcommune.plugin.api.core.WebControllerPlugin;
import org.jtalks.jcommune.plugin.api.filters.TypeFilter;
import org.springframework.context.ApplicationContextAware;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.method.HandlerMethodSelector;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Set;
import java.util.List;
import java.util.ArrayList;
import java.util.concurrent.ConcurrentHashMap;
/**
* Custom handler mapping. Needed to map plugin handlers separately from application handlers. It's necessary to allow
* update handlers without application restart. Default Spring {@code <mvc:annotation-driven />} still needs to be
* declared as usually - it will handle usual static controllers.
*
* @author Mikhail Stryzhonok
*/
public class PluginHandlerMapping extends RequestMappingHandlerMapping {
private static final PluginHandlerMapping INSTANCE = new PluginHandlerMapping();
private final Map<MethodAwareKey, HandlerMethod> pluginHandlerMethods = new ConcurrentHashMap<>();
private PluginLoader pluginLoader;
private PluginHandlerMapping() {
}
public static PluginHandlerMapping getInstance() {
return INSTANCE;
}
// need to run tests without context
@Override
protected boolean isContextRequired() {
return false;
}
/**
* {@inheritDoc}
*/
@Override
protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) {
if (PluginController.class.isAssignableFrom(method.getDeclaringClass())) {
registerPluginHandlerMethod((PluginController)handler, method, mapping);
}
}
/**
* Registers new plugin handler or updates existence
*
* @param handler controller object
* @param method method to be registered
* @param mapping information about request
*/
@VisibleForTesting
void registerPluginHandlerMethod(PluginController handler, Method method, RequestMappingInfo mapping) {
handler.setApiPath(this.getClass().getProtectionDomain().getCodeSource().getLocation().getPath());
Set<String> patterns = getMappingPathPatterns(mapping);
if (patterns.size() != 1) {
throw new IllegalStateException("Controller method " + method.getName() + " mapped to " + patterns.size()
+ " urls. Expected 1 url");
}
Set<RequestMethod> methods = mapping.getMethodsCondition().getMethods();
if (methods.size() != 1) {
throw new IllegalStateException("Controller method " + method.getName() + " mapped to " + methods.size()
+ " methods. Expected 1 method");
}
pluginHandlerMethods.put(new MethodAwareKey(methods.iterator().next(), getUniformUrl(patterns.iterator().next())),
createHandlerMethod(handler, method));
}
/**
* Adds handlers from controller to handler mapping
* Note: class should be annotated with {@link org.springframework.stereotype.Controller} annotation and all
* handler method should be annotated with {@link org.springframework.web.bind.annotation.RequestMapping} annotation
*
* @param controller controller object to map
*/
public void addController(PluginController controller) {
INSTANCE.detectHandlerMethods(controller);
if (controller instanceof ApplicationContextAware) {
((ApplicationContextAware) controller).setApplicationContext(getApplicationContext());
}
}
/**
* Disables handlers from specified controller
*
* @param controller controller bean to disable handlers
*/
public void deactivateController(PluginController controller) {
List<MethodAwareKey> keys = getPluginControllerUrls(controller);
for (MethodAwareKey key : keys) {
pluginHandlerMethods.remove(key);
}
}
/**
* Get list of URLs which can be handled by controller
*
* @param controller controller object
* @return list of URLs
*/
private List<MethodAwareKey> getPluginControllerUrls(PluginController controller) {
List<MethodAwareKey> keys = new ArrayList<>();
final Class controllerType = controller.getClass();
Set<Method> methods = HandlerMethodSelector.selectMethods(controllerType, new ReflectionUtils.MethodFilter() {
public boolean matches(Method method) {
return getMappingForMethod(method, controllerType) != null;
}
});
for (Method method : methods) {
RequestMappingInfo mapping = getMappingForMethod(method, controllerType);
//Method have {@link RequestMapping} annotation
if (mapping != null) {
Set<String> patterns = getMappingPathPatterns(mapping);
if (patterns.size() != 1) {
throw new IllegalStateException("Controller method " + method.getName() + " mapped to "
+ patterns.size() + " urls. Expected 1 url");
}
Set<RequestMethod> requestMethods = mapping.getMethodsCondition().getMethods();
if (requestMethods.size() != 1) {
throw new IllegalStateException("Controller method " + method.getName() + " mapped to " + methods.size()
+ " methods. Expected 1 method");
}
keys.add(new MethodAwareKey(requestMethods.iterator().next(), getUniformUrl(patterns.iterator().next())));
}
}
return keys;
}
/**
* {@inheritDoc}
*/
@Override
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
MethodAwareKey key = new MethodAwareKey(RequestMethod.valueOf(request.getMethod()), getUniformUrl(lookupPath));
//We should clear map in case if plugin version was changed
pluginHandlerMethods.clear();
//We should update Web plugins before resolving handler
pluginLoader.reloadPlugins(new TypeFilter(WebControllerPlugin.class));
HandlerMethod handlerMethod = findHandlerMethod(key);
if (handlerMethod != null) {
RequestMappingInfo mappingInfo = getMappingForMethod(handlerMethod.getMethod(), handlerMethod.getBeanType());
//IMPORTANT: Should be called to set request attributes which allows resolve path variables
handleMatch(mappingInfo, lookupPath, request);
return handlerMethod;
} else {
return super.getHandlerInternal(request);
}
}
protected HandlerMethod findHandlerMethod(MethodAwareKey key) {
//firstly try to find absolutely equal
HandlerMethod method = pluginHandlerMethods.get(key);
if (method != null) {
return method;
}
//if not found try to find matching
for (Map.Entry<MethodAwareKey, HandlerMethod> entry : pluginHandlerMethods.entrySet()) {
if (key.urlRegExp.matches(entry.getKey().urlRegExp) && key.getMethod() == entry.getKey().getMethod()) {
return entry.getValue();
}
}
return null;
}
/**
* Adds "/" to the end of url if it necessary. Needed to support optional "/" at the end
*/
private String getUniformUrl(String url) {
if (url.endsWith("/")) {
return url;
}
return url + "/";
}
//Needed for tests only
@VisibleForTesting
Map<MethodAwareKey, HandlerMethod> getPluginHandlerMethods() {
return pluginHandlerMethods;
}
public PluginLoader getPluginLoader() {
return pluginLoader;
}
public void setPluginLoader(PluginLoader pluginLoader) {
this.pluginLoader = pluginLoader;
}
static class MethodAwareKey {
private static String PATH_VARIABLE_REGEXP = "\\{(.*?)\\}";
private RequestMethod method;
private String urlRegExp;
public MethodAwareKey(RequestMethod method, String url) {
this.method = method;
this.urlRegExp = url.replaceAll(PATH_VARIABLE_REGEXP, "([^/]+)");
}
public RequestMethod getMethod() {
return method;
}
public String getUrlRegExp() {
return urlRegExp;
}
/**
* equals and hashCode needed for removing from map.
* for searching use {@link PluginHandlerMapping#findHandlerMethod(MethodAwareKey)} method
* this method compares urls using regexp
*/
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
MethodAwareKey that = (MethodAwareKey) o;
return method == that.method && !(urlRegExp != null ? !urlRegExp.equals(that.urlRegExp) : that.urlRegExp != null);
}
@Override
public int hashCode() {
int result = method != null ? method.hashCode() : 0;
result = 31 * result + (urlRegExp != null ? urlRegExp.hashCode() : 0);
return result;
}
}
}