/* * Copyright 2009-2010 the original author or authors. * * 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. */ package org.springframework.batch.admin.web.util; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import javax.servlet.http.HttpServletRequest; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.BeansException; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.util.ClassUtils; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.support.HandlerMethodResolver; import org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping; import org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping; import org.springframework.web.util.UrlPathHelper; /** * Component that discovers request mappings in its application context and * reveals their meta data. Any {@link RequestMapping} annotations in controller * components at method or type level are discovered. * * @author Dave Syer * */ @Controller public class HomeController implements ApplicationContextAware, InitializingBean { private static Log logger = LogFactory.getLog(HomeController.class); private ApplicationContext applicationContext; private Set<String> urls; private List<ResourceInfo> defaultResources; private List<ResourceInfo> jsonResources; private String servletPath; private Properties defaultProperties = null; private Properties jsonProperties = null; /** * * @see ApplicationContextAware#setApplicationContext(ApplicationContext) */ public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } /** * The path that will be added to the model as an attribute ("servletPath") * before rendering. Defaults to the parent servlet path (as defined in the * http servlet request). * * @param servletPath the servlet path to set */ public void setServletPath(String servletPath) { this.servletPath = servletPath; } /** * Pre-configured mapping from url path to description for default (HTML) * resources. * * @param defaultResources the default resources to set */ public void setDefaultResources(Properties defaultResources) { this.defaultProperties = defaultResources; } /** * Pre-configured mapping from url path to description for JSON resources. * If empty the description will be replaced with the one from the * {@link #setDefaultResources(Properties) default resources}. * * @param jsonResources the json resources to set */ public void setJsonResources(Properties jsonResources) { this.jsonProperties = jsonResources; } /** * Create the meta data by querying the context for mappings. * * @see InitializingBean#afterPropertiesSet() */ public void afterPropertiesSet() throws Exception { if (defaultProperties == null || defaultProperties.isEmpty()) { findResources(); } else { this.urls = buildUrlsFromProperties(defaultProperties); this.defaultResources = buildResourcesFromProperties(defaultProperties, defaultProperties); this.jsonResources = buildResourcesFromProperties(jsonProperties, defaultProperties); } } private List<ResourceInfo> buildResourcesFromProperties(Properties properties, Properties defaults) { Set<ResourceInfo> resources = new TreeSet<ResourceInfo>(); if (properties == null) { if (defaults == null) { return new ArrayList<ResourceInfo>(); } properties = defaults; } for (Enumeration<?> iterator = properties.propertyNames(); iterator.hasMoreElements();) { String key = (String) iterator.nextElement(); String method = key.substring(0, key.indexOf("/")); String url = key.substring(key.indexOf("/")); String description = properties.getProperty(key, defaults.getProperty(key)); resources.add(new ResourceInfo(url, RequestMethod.valueOf(method), description)); } return new ArrayList<ResourceInfo>(resources); } private Set<String> buildUrlsFromProperties(Properties properties) { Set<String> urls = new HashSet<String>(); if (properties == null) { return urls; } for (Enumeration<?> iterator = properties.propertyNames(); iterator.hasMoreElements();) { String key = (String) iterator.nextElement(); String url = key.substring(key.indexOf("/")); urls.add(url); } return urls; } private void findResources() { Map<String, Object> handlerMap = new HashMap<String, Object>(); DefaultAnnotationHandlerMapping annotationMapping = new DefaultAnnotationHandlerMapping(); annotationMapping.setApplicationContext(applicationContext); annotationMapping.initApplicationContext(); handlerMap.putAll(annotationMapping.getHandlerMap()); BeanNameUrlHandlerMapping beanMapping = new BeanNameUrlHandlerMapping(); beanMapping.setApplicationContext(applicationContext); beanMapping.initApplicationContext(); handlerMap.putAll(beanMapping.getHandlerMap()); this.urls = findUniqueUrls(handlerMap.keySet()); this.defaultResources = findMethods(handlerMap, this.urls); this.jsonResources = new ArrayList<ResourceInfo>(); for (Iterator<ResourceInfo> iterator = this.defaultResources.iterator(); iterator.hasNext();) { ResourceInfo info = (ResourceInfo) iterator.next(); if (info.getUrl().endsWith(".json")) { iterator.remove(); this.jsonResources.add(info); } } } private List<ResourceInfo> findMethods(Map<String, Object> handlerMap, Set<String> urls) { SortedSet<ResourceInfo> result = new TreeSet<ResourceInfo>(); for (String key : urls) { Object handler = handlerMap.get(key); Class<?> handlerType = ClassUtils.getUserClass(handler); HandlerMethodResolver resolver = new HandlerMethodResolver(); resolver.init(handlerType); String[] typeMappings = null; RequestMapping typeMapping = AnnotationUtils.findAnnotation(handlerType, RequestMapping.class); if (typeMapping != null) { typeMappings = typeMapping.value(); } Set<Method> handlerMethods = resolver.getHandlerMethods(); for (Method method : handlerMethods) { RequestMapping mapping = method.getAnnotation(RequestMapping.class); Collection<String> computedMappings = new HashSet<String>(); if (typeMappings != null) { computedMappings.addAll(Arrays.asList(typeMappings)); } for (String path : mapping.value()) { if (typeMappings != null) { for (String parent : computedMappings) { if (parent.endsWith("/")) { parent = parent.substring(0, parent.length() - 1); } computedMappings.add(parent + path); } } else { computedMappings.add(path); } } logger.debug("Analysing mappings for method:" + method.getName() + ", key:" + key + ", computed mappings: " + computedMappings); if (computedMappings.contains(key)) { RequestMethod[] methods = mapping.method(); if (methods != null && methods.length > 0) { for (RequestMethod requestMethod : methods) { logger.debug("Added explicit mapping for path=" + key + "to RequestMethod=" + requestMethod); result.add(new ResourceInfo(key, requestMethod)); } } else { logger.debug("Added implicit mapping for path=" + key + "to RequestMethod=GET"); result.add(new ResourceInfo(key, RequestMethod.GET)); } } } if (handlerMethods.isEmpty()) { result.add(new ResourceInfo(key, RequestMethod.GET)); } } return new ArrayList<ResourceInfo>(result); } private Set<String> findUniqueUrls(Collection<String> inputs) { Set<String> result = new HashSet<String>(inputs); for (String url : inputs) { String extended = url + ".*"; if (inputs.contains(extended)) { result.remove(extended); } extended = url + "/"; if (inputs.contains(extended)) { result.remove(extended); } } return result; } /** * Inspect the handler mapping at the level of HTTP {@link RequestMethod}. * Each URI pattern that is mapped can be mapped to multiple request * methods. If the mapping is not explicit this method only returns GET * (even though technically it would respond to POST as well). * * @param request the current servlet request (used to extract a page * attribute "sevletPath") * @param model {@link org.springframework.ui.ModelMap} to be used * * @return a map of URI pattern to request methods accepted */ @RequestMapping(value = { "/home" }, method = RequestMethod.GET) public String getResources(HttpServletRequest request, ModelMap model) { String servletPath = this.servletPath; if (servletPath == null) { servletPath = new UrlPathHelper().getServletPath(request); } model.addAttribute("servletPath", servletPath); List<ResourceInfo> resources = new ArrayList<ResourceInfo>(); if (!request.getRequestURI().endsWith(".json")) { resources.addAll(defaultResources); } resources.addAll(jsonResources); model.addAttribute("resources", resources); return "home"; } /** * The set of unique URI patterns mapped, excluding implicit mappings. * Implicit mappings include all the values here plus patterns created from * them by appending "/" (if not already present) and ".*" (if no suffix is * already provided). * * @return the set of unique URI patterns mapped */ public Set<String> getUrlPatterns() { return urls; } }