From af47a8b79b0fa07ff2c038197a07c09b745802ac Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Mon, 27 Oct 2008 09:13:23 +0000 Subject: [PATCH] Initial import of web servlet module --- org.springframework.web.servlet/build.xml | 6 + org.springframework.web.servlet/ivy.xml | 53 + org.springframework.web.servlet/pom.xml | 86 + .../web/bind/EscapedErrors.java | 210 ++ ...ssingServletRequestParameterException.java | 62 + .../web/bind/RequestUtils.java | 491 ++++ .../bind/ServletRequestBindingException.java | 51 + .../web/bind/ServletRequestDataBinder.java | 127 + ...ServletRequestParameterPropertyValues.java | 80 + .../web/bind/ServletRequestUtils.java | 697 +++++ .../web/bind/WebDataBinder.java | 227 ++ .../web/bind/annotation/InitBinder.java | 61 + .../web/bind/annotation/ModelAttribute.java | 59 + .../web/bind/annotation/RequestMapping.java | 230 ++ .../web/bind/annotation/RequestMethod.java | 41 + .../web/bind/annotation/RequestParam.java | 55 + .../bind/annotation/SessionAttributes.java | 71 + .../web/bind/annotation/package.html | 8 + .../HandlerMethodInvocationException.java | 41 + .../support/HandlerMethodInvoker.java | 487 ++++ .../support/HandlerMethodResolver.java | 137 + .../web/bind/annotation/support/package.html | 7 + .../org/springframework/web/bind/package.html | 7 + .../ConfigurableWebBindingInitializer.java | 135 + .../support/DefaultSessionAttributeStore.java | 84 + .../bind/support/SessionAttributeStore.java | 59 + .../web/bind/support/SessionStatus.java | 47 + .../web/bind/support/SimpleSessionStatus.java | 39 + .../web/bind/support/WebArgumentResolver.java | 63 + .../bind/support/WebBindingInitializer.java | 38 + .../bind/support/WebRequestDataBinder.java | 125 + .../web/bind/support/package.html | 7 + .../MaxUploadSizeExceededException.java | 57 + .../web/multipart/MultipartException.java | 49 + .../web/multipart/MultipartFile.java | 103 + .../MultipartHttpServletRequest.java | 47 + .../web/multipart/MultipartRequest.java | 58 + .../web/multipart/MultipartResolver.java | 127 + .../commons/CommonsFileUploadSupport.java | 321 +++ .../commons/CommonsMultipartFile.java | 194 ++ .../commons/CommonsMultipartResolver.java | 205 ++ .../web/multipart/commons/package.html | 8 + .../web/multipart/package.html | 10 + .../AbstractMultipartHttpServletRequest.java | 92 + .../support/ByteArrayMultipartFileEditor.java | 58 + .../DefaultMultipartHttpServletRequest.java | 121 + .../multipart/support/MultipartFilter.java | 150 ++ .../support/StringMultipartFileEditor.java | 78 + .../web/multipart/support/package.html | 9 + .../web/servlet/DispatcherServlet.java | 1294 ++++++++++ .../web/servlet/DispatcherServlet.properties | 19 + .../web/servlet/FrameworkServlet.java | 647 +++++ .../web/servlet/HandlerAdapter.java | 86 + .../web/servlet/HandlerExceptionResolver.java | 49 + .../web/servlet/HandlerExecutionChain.java | 122 + .../web/servlet/HandlerInterceptor.java | 120 + .../web/servlet/HandlerMapping.java | 85 + .../web/servlet/HttpServletBean.java | 213 ++ .../web/servlet/LocaleResolver.java | 61 + .../web/servlet/ModelAndView.java | 303 +++ .../ModelAndViewDefiningException.java | 57 + .../servlet/RequestToViewNameTranslator.java | 41 + .../web/servlet/ResourceServlet.java | 342 +++ .../web/servlet/ThemeResolver.java | 67 + .../org/springframework/web/servlet/View.java | 68 + .../web/servlet/ViewRendererServlet.java | 114 + .../web/servlet/ViewResolver.java | 55 + .../AbstractDetectingUrlHandlerMapping.java | 101 + .../handler/AbstractHandlerMapping.java | 238 ++ .../handler/AbstractUrlHandlerMapping.java | 362 +++ .../handler/BeanNameUrlHandlerMapping.java | 70 + .../handler/DispatcherServletWebRequest.java | 51 + .../handler/HandlerInterceptorAdapter.java | 58 + .../SimpleMappingExceptionResolver.java | 446 ++++ .../handler/SimpleServletHandlerAdapter.java | 71 + .../handler/SimpleServletPostProcessor.java | 157 ++ .../handler/SimpleUrlHandlerMapping.java | 134 + .../UserRoleAuthorizationInterceptor.java | 78 + .../WebRequestHandlerInterceptorAdapter.java | 71 + .../AbstractPathMapHandlerMapping.java | 201 ++ .../CommonsPathMapHandlerMapping.java | 81 + .../web/servlet/handler/metadata/PathMap.java | 58 + .../web/servlet/handler/metadata/package.html | 37 + .../web/servlet/handler/package.html | 8 + .../servlet/i18n/AbstractLocaleResolver.java | 49 + .../i18n/AcceptHeaderLocaleResolver.java | 49 + .../servlet/i18n/CookieLocaleResolver.java | 146 ++ .../web/servlet/i18n/FixedLocaleResolver.java | 68 + .../servlet/i18n/LocaleChangeInterceptor.java | 82 + .../servlet/i18n/SessionLocaleResolver.java | 85 + .../web/servlet/i18n/package.html | 9 + .../mvc/AbstractCommandController.java | 105 + .../web/servlet/mvc/AbstractController.java | 164 ++ .../servlet/mvc/AbstractFormController.java | 674 +++++ .../mvc/AbstractUrlViewController.java | 111 + .../mvc/AbstractWizardFormController.java | 743 ++++++ .../servlet/mvc/BaseCommandController.java | 591 +++++ .../mvc/CancellableFormController.java | 205 ++ .../web/servlet/mvc/Controller.java | 133 + .../mvc/HttpRequestHandlerAdapter.java | 60 + .../web/servlet/mvc/LastModified.java | 59 + .../mvc/ParameterizableViewController.java | 104 + .../mvc/ServletForwardingController.java | 155 ++ .../mvc/ServletWrappingController.java | 196 ++ .../mvc/SimpleControllerHandlerAdapter.java | 58 + .../web/servlet/mvc/SimpleFormController.java | 462 ++++ .../mvc/UrlFilenameViewController.java | 163 ++ .../servlet/mvc/WebContentInterceptor.java | 199 ++ .../AnnotationMethodHandlerAdapter.java | 681 +++++ .../DefaultAnnotationHandlerMapping.java | 215 ++ .../ServletAnnotationMappingUtils.java | 85 + .../web/servlet/mvc/annotation/package.html | 7 + .../AbstractUrlMethodNameResolver.java | 115 + .../InternalPathMethodNameResolver.java | 123 + .../mvc/multiaction/MethodNameResolver.java | 45 + .../multiaction/MultiActionController.java | 665 +++++ .../NoSuchRequestHandlingMethodException.java | 78 + .../ParameterMethodNameResolver.java | 223 ++ .../PropertiesMethodNameResolver.java | 96 + .../web/servlet/mvc/multiaction/package.html | 23 + .../web/servlet/mvc/package.html | 44 + .../AbstractControllerUrlHandlerMapping.java | 183 ++ .../AnnotationControllerTypePredicate.java | 41 + .../ControllerBeanNameHandlerMapping.java | 94 + .../ControllerClassNameHandlerMapping.java | 183 ++ .../mvc/support/ControllerTypePredicate.java | 40 + .../web/servlet/mvc/support/package.html | 8 + .../mvc/throwaway/ThrowawayController.java | 69 + .../ThrowawayControllerHandlerAdapter.java | 134 + .../web/servlet/mvc/throwaway/package.html | 8 + .../springframework/web/servlet/package.html | 13 + .../web/servlet/support/BindStatus.java | 336 +++ .../support/JspAwareRequestContext.java | 116 + .../web/servlet/support/JstlUtils.java | 144 ++ .../web/servlet/support/RequestContext.java | 737 ++++++ .../servlet/support/RequestContextUtils.java | 159 ++ .../servlet/support/WebContentGenerator.java | 351 +++ .../web/servlet/support/package.html | 9 + .../web/servlet/tags/BindErrorsTag.java | 93 + .../web/servlet/tags/BindTag.java | 183 ++ .../web/servlet/tags/EditorAwareTag.java | 42 + .../web/servlet/tags/EscapeBodyTag.java | 109 + .../web/servlet/tags/HtmlEscapeTag.java | 55 + .../servlet/tags/HtmlEscapingAwareTag.java | 77 + .../web/servlet/tags/MessageTag.java | 303 +++ .../web/servlet/tags/NestedPathTag.java | 119 + .../servlet/tags/RequestContextAwareTag.java | 118 + .../web/servlet/tags/ThemeTag.java | 59 + .../web/servlet/tags/TransformTag.java | 139 + .../tags/form/AbstractCheckedElementTag.java | 91 + .../form/AbstractDataBoundFormElementTag.java | 227 ++ .../servlet/tags/form/AbstractFormTag.java | 126 + .../tags/form/AbstractHtmlElementBodyTag.java | 156 ++ .../tags/form/AbstractHtmlElementTag.java | 420 +++ .../form/AbstractHtmlInputElementTag.java | 207 ++ .../form/AbstractMultiCheckedElementTag.java | 280 ++ .../form/AbstractSingleCheckedElementTag.java | 105 + .../web/servlet/tags/form/CheckboxTag.java | 90 + .../web/servlet/tags/form/CheckboxesTag.java | 55 + .../web/servlet/tags/form/ErrorsTag.java | 191 ++ .../web/servlet/tags/form/FormTag.java | 424 +++ .../web/servlet/tags/form/HiddenInputTag.java | 51 + .../web/servlet/tags/form/InputTag.java | 172 ++ .../web/servlet/tags/form/LabelTag.java | 146 ++ .../web/servlet/tags/form/OptionTag.java | 243 ++ .../web/servlet/tags/form/OptionWriter.java | 249 ++ .../web/servlet/tags/form/OptionsTag.java | 203 ++ .../servlet/tags/form/PasswordInputTag.java | 70 + .../web/servlet/tags/form/RadioButtonTag.java | 43 + .../servlet/tags/form/RadioButtonsTag.java | 36 + .../web/servlet/tags/form/SelectTag.java | 308 +++ .../tags/form/SelectedValueComparator.java | 205 ++ .../web/servlet/tags/form/TagIdGenerator.java | 52 + .../web/servlet/tags/form/TagWriter.java | 261 ++ .../web/servlet/tags/form/TextareaTag.java | 103 + .../web/servlet/tags/form/ValueFormatter.java | 74 + .../web/servlet/tags/form/package.html | 9 + .../web/servlet/tags/form/spring-form.tld | 2275 +++++++++++++++++ .../web/servlet/tags/package.html | 9 + .../web/servlet/tags/spring.tld | 345 +++ .../servlet/theme/AbstractThemeResolver.java | 54 + .../servlet/theme/CookieThemeResolver.java | 110 + .../web/servlet/theme/FixedThemeResolver.java | 44 + .../servlet/theme/SessionThemeResolver.java | 59 + .../servlet/theme/ThemeChangeInterceptor.java | 77 + .../web/servlet/theme/package.html | 23 + .../view/AbstractCachingViewResolver.java | 177 ++ .../servlet/view/AbstractTemplateView.java | 214 ++ .../view/AbstractTemplateViewResolver.java | 110 + .../servlet/view/AbstractUrlBasedView.java | 85 + .../web/servlet/view/AbstractView.java | 391 +++ .../servlet/view/BeanNameViewResolver.java | 78 + .../DefaultRequestToViewNameTranslator.java | 189 ++ .../servlet/view/InternalResourceView.java | 336 +++ .../view/InternalResourceViewResolver.java | 132 + .../web/servlet/view/JstlView.java | 137 + .../web/servlet/view/RedirectView.java | 395 +++ .../view/ResourceBundleViewResolver.java | 280 ++ .../servlet/view/UrlBasedViewResolver.java | 442 ++++ .../web/servlet/view/XmlViewResolver.java | 156 ++ .../view/document/AbstractExcelView.java | 221 ++ .../view/document/AbstractJExcelView.java | 176 ++ .../view/document/AbstractPdfStamperView.java | 110 + .../view/document/AbstractPdfView.java | 183 ++ .../web/servlet/view/document/package.html | 8 + .../view/freemarker/FreeMarkerConfig.java | 50 + .../view/freemarker/FreeMarkerConfigurer.java | 142 + .../view/freemarker/FreeMarkerView.java | 383 +++ .../freemarker/FreeMarkerViewResolver.java | 60 + .../web/servlet/view/freemarker/package.html | 10 + .../web/servlet/view/freemarker/spring.ftl | 366 +++ ...AbstractJasperReportsSingleFormatView.java | 159 ++ .../AbstractJasperReportsView.java | 847 ++++++ .../ConfigurableJasperReportsView.java | 90 + .../jasperreports/JasperReportsCsvView.java | 44 + .../jasperreports/JasperReportsHtmlView.java | 44 + .../JasperReportsMultiFormatView.java | 234 ++ .../jasperreports/JasperReportsPdfView.java | 44 + .../JasperReportsViewResolver.java | 136 + .../jasperreports/JasperReportsXlsView.java | 44 + .../servlet/view/jasperreports/package.html | 10 + .../web/servlet/view/package.html | 14 + .../tiles2/AbstractSpringPreparerFactory.java | 71 + .../tiles2/SimpleSpringPreparerFactory.java | 70 + .../tiles2/SpringBeanPreparerFactory.java | 41 + .../view/tiles2/SpringLocaleResolver.java | 52 + .../servlet/view/tiles2/TilesConfigurer.java | 250 ++ .../web/servlet/view/tiles2/TilesView.java | 78 + .../web/servlet/view/tiles2/package.html | 10 + .../servlet/view/velocity/VelocityConfig.java | 39 + .../view/velocity/VelocityConfigurer.java | 147 ++ .../view/velocity/VelocityLayoutView.java | 181 ++ .../velocity/VelocityLayoutViewResolver.java | 106 + .../view/velocity/VelocityToolboxView.java | 147 ++ .../servlet/view/velocity/VelocityView.java | 602 +++++ .../view/velocity/VelocityViewResolver.java | 152 ++ .../web/servlet/view/velocity/package.html | 10 + .../web/servlet/view/velocity/spring.vm | 313 +++ .../servlet/view/xslt/AbstractXsltView.java | 590 +++++ .../web/servlet/view/xslt/XsltView.java | 502 ++++ .../servlet/view/xslt/XsltViewResolver.java | 139 + .../web/servlet/view/xslt/package.html | 8 + .../src/main/java/overview.html | 7 + .../src/test/resources/log4j.xml | 28 + org.springframework.web.servlet/template.mf | 54 + 245 files changed, 40222 insertions(+) create mode 100644 org.springframework.web.servlet/build.xml create mode 100644 org.springframework.web.servlet/ivy.xml create mode 100644 org.springframework.web.servlet/pom.xml create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/bind/EscapedErrors.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/bind/MissingServletRequestParameterException.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/bind/RequestUtils.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/bind/ServletRequestBindingException.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/bind/ServletRequestParameterPropertyValues.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/bind/ServletRequestUtils.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/bind/WebDataBinder.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/InitBinder.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/ModelAttribute.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/RequestMethod.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/RequestParam.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/SessionAttributes.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/package.html create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvocationException.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/support/package.html create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/bind/package.html create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/bind/support/ConfigurableWebBindingInitializer.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/bind/support/DefaultSessionAttributeStore.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/bind/support/SessionAttributeStore.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/bind/support/SessionStatus.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/bind/support/SimpleSessionStatus.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/bind/support/WebArgumentResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/bind/support/WebBindingInitializer.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/bind/support/package.html create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/MaxUploadSizeExceededException.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/MultipartException.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/MultipartFile.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/MultipartHttpServletRequest.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/MultipartRequest.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/MultipartResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/commons/CommonsFileUploadSupport.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/commons/CommonsMultipartFile.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/commons/CommonsMultipartResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/commons/package.html create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/package.html create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/support/AbstractMultipartHttpServletRequest.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/support/ByteArrayMultipartFileEditor.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/support/DefaultMultipartHttpServletRequest.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/support/MultipartFilter.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/support/StringMultipartFileEditor.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/support/package.html create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/DispatcherServlet.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/DispatcherServlet.properties create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/FrameworkServlet.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/HandlerAdapter.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/HandlerExceptionResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/HandlerExecutionChain.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/HandlerInterceptor.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/HandlerMapping.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/HttpServletBean.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/LocaleResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/ModelAndView.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/ModelAndViewDefiningException.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/RequestToViewNameTranslator.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/ResourceServlet.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/ThemeResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/View.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/ViewRendererServlet.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/ViewResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/AbstractDetectingUrlHandlerMapping.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/AbstractUrlHandlerMapping.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/BeanNameUrlHandlerMapping.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/DispatcherServletWebRequest.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/HandlerInterceptorAdapter.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/SimpleMappingExceptionResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/SimpleServletHandlerAdapter.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/SimpleServletPostProcessor.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/SimpleUrlHandlerMapping.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/UserRoleAuthorizationInterceptor.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/WebRequestHandlerInterceptorAdapter.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/metadata/AbstractPathMapHandlerMapping.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/metadata/CommonsPathMapHandlerMapping.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/metadata/PathMap.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/metadata/package.html create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/package.html create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/i18n/AbstractLocaleResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/i18n/AcceptHeaderLocaleResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/i18n/CookieLocaleResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/i18n/FixedLocaleResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/i18n/LocaleChangeInterceptor.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/i18n/SessionLocaleResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/i18n/package.html create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/AbstractCommandController.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/AbstractController.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/AbstractFormController.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/AbstractUrlViewController.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/AbstractWizardFormController.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/BaseCommandController.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/CancellableFormController.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/Controller.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/HttpRequestHandlerAdapter.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/LastModified.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/ParameterizableViewController.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/ServletForwardingController.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/ServletWrappingController.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/SimpleControllerHandlerAdapter.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/SimpleFormController.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/UrlFilenameViewController.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/WebContentInterceptor.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/DefaultAnnotationHandlerMapping.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationMappingUtils.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/package.html create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/multiaction/AbstractUrlMethodNameResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/multiaction/InternalPathMethodNameResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/multiaction/MethodNameResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/multiaction/MultiActionController.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/multiaction/NoSuchRequestHandlingMethodException.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/multiaction/ParameterMethodNameResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/multiaction/PropertiesMethodNameResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/multiaction/package.html create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/package.html create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/support/AbstractControllerUrlHandlerMapping.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/support/AnnotationControllerTypePredicate.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/support/ControllerBeanNameHandlerMapping.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/support/ControllerClassNameHandlerMapping.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/support/ControllerTypePredicate.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/support/package.html create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/throwaway/ThrowawayController.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/throwaway/ThrowawayControllerHandlerAdapter.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/throwaway/package.html create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/package.html create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/support/BindStatus.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/support/JspAwareRequestContext.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/support/JstlUtils.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/support/RequestContext.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/support/RequestContextUtils.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/support/WebContentGenerator.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/support/package.html create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/BindErrorsTag.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/BindTag.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/EditorAwareTag.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/EscapeBodyTag.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/HtmlEscapeTag.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/HtmlEscapingAwareTag.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/MessageTag.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/NestedPathTag.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/RequestContextAwareTag.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/ThemeTag.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/TransformTag.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/AbstractCheckedElementTag.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/AbstractDataBoundFormElementTag.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/AbstractFormTag.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/AbstractHtmlElementBodyTag.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/AbstractHtmlElementTag.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/AbstractHtmlInputElementTag.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/AbstractMultiCheckedElementTag.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/AbstractSingleCheckedElementTag.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/CheckboxTag.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/CheckboxesTag.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/ErrorsTag.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/FormTag.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/HiddenInputTag.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/InputTag.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/LabelTag.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/OptionTag.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/OptionWriter.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/OptionsTag.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/PasswordInputTag.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/RadioButtonTag.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/RadioButtonsTag.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/SelectTag.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/SelectedValueComparator.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/TagIdGenerator.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/TagWriter.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/TextareaTag.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/ValueFormatter.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/package.html create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/spring-form.tld create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/package.html create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/spring.tld create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/theme/AbstractThemeResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/theme/CookieThemeResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/theme/FixedThemeResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/theme/SessionThemeResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/theme/ThemeChangeInterceptor.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/theme/package.html create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/AbstractCachingViewResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/AbstractTemplateView.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/AbstractTemplateViewResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/AbstractUrlBasedView.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/AbstractView.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/BeanNameViewResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/DefaultRequestToViewNameTranslator.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/InternalResourceView.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/InternalResourceViewResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/JstlView.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/RedirectView.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/ResourceBundleViewResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/UrlBasedViewResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/XmlViewResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/document/AbstractExcelView.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/document/AbstractJExcelView.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/document/AbstractPdfStamperView.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/document/AbstractPdfView.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/document/package.html create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerConfig.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerConfigurer.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerView.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerViewResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/freemarker/package.html create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/freemarker/spring.ftl create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/jasperreports/AbstractJasperReportsSingleFormatView.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/jasperreports/AbstractJasperReportsView.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/jasperreports/ConfigurableJasperReportsView.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/jasperreports/JasperReportsCsvView.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/jasperreports/JasperReportsHtmlView.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/jasperreports/JasperReportsMultiFormatView.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/jasperreports/JasperReportsPdfView.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/jasperreports/JasperReportsViewResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/jasperreports/JasperReportsXlsView.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/jasperreports/package.html create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/package.html create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/tiles2/AbstractSpringPreparerFactory.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/tiles2/SimpleSpringPreparerFactory.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/tiles2/SpringBeanPreparerFactory.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/tiles2/SpringLocaleResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/tiles2/TilesConfigurer.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/tiles2/TilesView.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/tiles2/package.html create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/velocity/VelocityConfig.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/velocity/VelocityConfigurer.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/velocity/VelocityLayoutView.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/velocity/VelocityLayoutViewResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/velocity/VelocityToolboxView.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/velocity/VelocityView.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/velocity/VelocityViewResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/velocity/package.html create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/velocity/spring.vm create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/xslt/AbstractXsltView.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/xslt/XsltView.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/xslt/XsltViewResolver.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/xslt/package.html create mode 100644 org.springframework.web.servlet/src/main/java/overview.html create mode 100644 org.springframework.web.servlet/src/test/resources/log4j.xml create mode 100644 org.springframework.web.servlet/template.mf diff --git a/org.springframework.web.servlet/build.xml b/org.springframework.web.servlet/build.xml new file mode 100644 index 00000000000..b1807ec3baf --- /dev/null +++ b/org.springframework.web.servlet/build.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/org.springframework.web.servlet/ivy.xml b/org.springframework.web.servlet/ivy.xml new file mode 100644 index 00000000000..c6ee9b41dad --- /dev/null +++ b/org.springframework.web.servlet/ivy.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/org.springframework.web.servlet/pom.xml b/org.springframework.web.servlet/pom.xml new file mode 100644 index 00000000000..a00a8275bf8 --- /dev/null +++ b/org.springframework.web.servlet/pom.xml @@ -0,0 +1,86 @@ + + + + org.springframework + org.springframework.parent + 3.0-M1-SNAPSHOT + + 4.0.0 + org.springframework.web + jar + Spring Framework: Web + + + org.springframework + org.springframework.core + + + org.springframework + org.springframework.beans + + + org.springframework + org.springframework.aop + + + org.springframework + org.springframework.context + + + javax.servlet + com.springsource.javax.servlet + true + + + javax.el + com.springsource.javax.el + true + + + javax.servlet + com.springsource.javax.servlet.jsp + true + + + javax.xml.rpc + com.springsource.javax.xml.rpc + true + + + org.aopalliance + com.springsource.org.aopalliance + true + + + org.apache.myfaces + com.springsource.org.apache.myfaces.javax.faces + true + + + com.caucho + com.springsource.com.caucho + true + + + org.apache.log4j + com.springsource.org.apache.log4j + compile + true + + + org.apache.commons + com.springsource.org.apache.commons.httpclient + true + + + org.apache.axis + com.springsource.org.apache.axis + true + + + org.apache.taglibs + com.springsource.org.apache.taglibs.standard + true + + + \ No newline at end of file diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/EscapedErrors.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/EscapedErrors.java new file mode 100644 index 00000000000..111f27ee0f6 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/EscapedErrors.java @@ -0,0 +1,210 @@ +/* + * Copyright 2002-2006 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.web.bind; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.springframework.validation.Errors; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; +import org.springframework.web.util.HtmlUtils; + +/** + * Errors wrapper that adds automatic HTML escaping to the wrapped instance, + * for convenient usage in HTML views. Can be retrieved easily via + * RequestContext's getErrors method. + * + *

Note that BindTag does not use this class to avoid unnecessary + * creation of ObjectError instances. It just escapes the messages and values + * that get copied into the respective BindStatus instance. + * + * @author Juergen Hoeller + * @since 01.03.2003 + * @see org.springframework.web.servlet.support.RequestContext#getErrors + * @see org.springframework.web.servlet.tags.BindTag + */ +public class EscapedErrors implements Errors { + + private final Errors source; + + + /** + * Create a new EscapedErrors instance for the given source instance. + */ + public EscapedErrors(Errors source) { + if (source == null) { + throw new IllegalArgumentException("Cannot wrap a null instance"); + } + this.source = source; + } + + public Errors getSource() { + return this.source; + } + + + public String getObjectName() { + return this.source.getObjectName(); + } + + public void setNestedPath(String nestedPath) { + this.source.setNestedPath(nestedPath); + } + + public String getNestedPath() { + return this.source.getNestedPath(); + } + + public void pushNestedPath(String subPath) { + this.source.pushNestedPath(subPath); + } + + public void popNestedPath() throws IllegalStateException { + this.source.popNestedPath(); + } + + + public void reject(String errorCode) { + this.source.reject(errorCode); + } + + public void reject(String errorCode, String defaultMessage) { + this.source.reject(errorCode, defaultMessage); + } + + public void reject(String errorCode, Object[] errorArgs, String defaultMessage) { + this.source.reject(errorCode, errorArgs, defaultMessage); + } + + public void rejectValue(String field, String errorCode) { + this.source.rejectValue(field, errorCode); + } + + public void rejectValue(String field, String errorCode, String defaultMessage) { + this.source.rejectValue(field, errorCode, defaultMessage); + } + + public void rejectValue(String field, String errorCode, Object[] errorArgs, String defaultMessage) { + this.source.rejectValue(field, errorCode, errorArgs, defaultMessage); + } + + public void addAllErrors(Errors errors) { + this.source.addAllErrors(errors); + } + + + public boolean hasErrors() { + return this.source.hasErrors(); + } + + public int getErrorCount() { + return this.source.getErrorCount(); + } + + public List getAllErrors() { + return escapeObjectErrors(this.source.getAllErrors()); + } + + public boolean hasGlobalErrors() { + return this.source.hasGlobalErrors(); + } + + public int getGlobalErrorCount() { + return this.source.getGlobalErrorCount(); + } + + public List getGlobalErrors() { + return escapeObjectErrors(this.source.getGlobalErrors()); + } + + public ObjectError getGlobalError() { + return escapeObjectError(this.source.getGlobalError()); + } + + public boolean hasFieldErrors() { + return this.source.hasFieldErrors(); + } + + public int getFieldErrorCount() { + return this.source.getFieldErrorCount(); + } + + public List getFieldErrors() { + return this.source.getFieldErrors(); + } + + public FieldError getFieldError() { + return this.source.getFieldError(); + } + + public boolean hasFieldErrors(String field) { + return this.source.hasFieldErrors(field); + } + + public int getFieldErrorCount(String field) { + return this.source.getFieldErrorCount(field); + } + + public List getFieldErrors(String field) { + return escapeObjectErrors(this.source.getFieldErrors(field)); + } + + public FieldError getFieldError(String field) { + return (FieldError) escapeObjectError(this.source.getFieldError(field)); + } + + public Object getFieldValue(String field) { + Object value = this.source.getFieldValue(field); + return (value instanceof String ? HtmlUtils.htmlEscape((String) value) : value); + } + + public Class getFieldType(String field) { + return this.source.getFieldType(field); + } + + private ObjectError escapeObjectError(ObjectError source) { + if (source == null) { + return null; + } + if (source instanceof FieldError) { + FieldError fieldError = (FieldError) source; + Object value = fieldError.getRejectedValue(); + if (value instanceof String) { + value = HtmlUtils.htmlEscape((String) value); + } + return new FieldError( + fieldError.getObjectName(), fieldError.getField(), value, + fieldError.isBindingFailure(), fieldError.getCodes(), + fieldError.getArguments(), HtmlUtils.htmlEscape(fieldError.getDefaultMessage())); + } + return new ObjectError( + source.getObjectName(), source.getCodes(), source.getArguments(), + HtmlUtils.htmlEscape(source.getDefaultMessage())); + } + + private List escapeObjectErrors(List source) { + List escaped = new ArrayList(source.size()); + for (Iterator it = source.iterator(); it.hasNext();) { + ObjectError objectError = (ObjectError)it.next(); + escaped.add(escapeObjectError(objectError)); + } + return escaped; + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/MissingServletRequestParameterException.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/MissingServletRequestParameterException.java new file mode 100644 index 00000000000..6c1f68bd333 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/MissingServletRequestParameterException.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2007 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.web.bind; + +/** + * {@link ServletRequestBindingException} subclass that indicates a missing parameter. + * + * @author Juergen Hoeller + * @since 2.0.2 + */ +public class MissingServletRequestParameterException extends ServletRequestBindingException { + + private String parameterName; + + private String parameterType; + + + /** + * Constructor for MissingServletRequestParameterException. + * @param parameterName the name of the missing parameter + * @param parameterType the expected type of the missing parameter + */ + public MissingServletRequestParameterException(String parameterName, String parameterType) { + super(""); + this.parameterName = parameterName; + this.parameterType = parameterType; + } + + + public String getMessage() { + return "Required " + this.parameterType + " parameter '" + this.parameterName + "' is not present"; + } + + /** + * Return the name of the offending parameter. + */ + public String getParameterName() { + return this.parameterName; + } + + /** + * Return the expected type of the offending parameter. + */ + public String getParameterType() { + return this.parameterType; + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/RequestUtils.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/RequestUtils.java new file mode 100644 index 00000000000..a76ee8abde1 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/RequestUtils.java @@ -0,0 +1,491 @@ +/* + * Copyright 2002-2007 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.web.bind; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; + +import org.springframework.web.HttpRequestMethodNotSupportedException; + +/** + * Parameter extraction methods, for an approach distinct from data binding, + * in which parameters of specific types are required. + * + *

This approach is very useful for simple submissions, where binding + * request parameters to a command object would be overkill. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Keith Donald + * @deprecated as of Spring 2.0: use ServletRequestUtils instead + * @see ServletRequestUtils + */ +public abstract class RequestUtils { + + /** + * Throw a ServletException if the given HTTP request method should be rejected. + * @param request request to check + * @param method method (such as "GET") which should be rejected + * @throws ServletException if the given HTTP request is rejected + */ + public static void rejectRequestMethod(HttpServletRequest request, String method) throws ServletException { + if (request.getMethod().equals(method)) { + throw new HttpRequestMethodNotSupportedException(method); + } + } + + + /** + * Get an Integer parameter, or null if not present. + * Throws an exception if it the parameter value isn't a number. + * @param request current HTTP request + * @param name the name of the parameter + * @return the Integer value, or null if not present + * @throws ServletRequestBindingException a subclass of ServletException, + * so it doesn't need to be caught + */ + public static Integer getIntParameter(HttpServletRequest request, String name) + throws ServletRequestBindingException { + + return ServletRequestUtils.getIntParameter(request, name); + } + + /** + * Get an int parameter, with a fallback value. Never throws an exception. + * Can pass a distinguished value as default to enable checks of whether it was supplied. + * @param request current HTTP request + * @param name the name of the parameter + * @param defaultVal the default value to use as fallback + */ + public static int getIntParameter(HttpServletRequest request, String name, int defaultVal) { + return ServletRequestUtils.getIntParameter(request, name, defaultVal); + } + + /** + * Get an array of int parameters, return an empty array if not found. + * @param request current HTTP request + * @param name the name of the parameter with multiple possible values + */ + public static int[] getIntParameters(HttpServletRequest request, String name) { + return ServletRequestUtils.getIntParameters(request, name); + } + + /** + * Get an int parameter, throwing an exception if it isn't found or isn't a number. + * @param request current HTTP request + * @param name the name of the parameter + * @throws ServletRequestBindingException a subclass of ServletException, + * so it doesn't need to be caught + */ + public static int getRequiredIntParameter(HttpServletRequest request, String name) + throws ServletRequestBindingException { + + return ServletRequestUtils.getRequiredIntParameter(request, name); + } + + /** + * Get an array of int parameters, throwing an exception if not found or one is not a number.. + * @param request current HTTP request + * @param name the name of the parameter with multiple possible values + * @throws ServletRequestBindingException a subclass of ServletException, + * so it doesn't need to be caught + */ + public static int[] getRequiredIntParameters(HttpServletRequest request, String name) + throws ServletRequestBindingException { + + return ServletRequestUtils.getRequiredIntParameters(request, name); + } + + + /** + * Get a Long parameter, or null if not present. + * Throws an exception if it the parameter value isn't a number. + * @param request current HTTP request + * @param name the name of the parameter + * @return the Long value, or null if not present + * @throws ServletRequestBindingException a subclass of ServletException, + * so it doesn't need to be caught + */ + public static Long getLongParameter(HttpServletRequest request, String name) + throws ServletRequestBindingException { + + return ServletRequestUtils.getLongParameter(request, name); + } + + /** + * Get a long parameter, with a fallback value. Never throws an exception. + * Can pass a distinguished value as default to enable checks of whether it was supplied. + * @param request current HTTP request + * @param name the name of the parameter + * @param defaultVal the default value to use as fallback + */ + public static long getLongParameter(HttpServletRequest request, String name, long defaultVal) { + return ServletRequestUtils.getLongParameter(request, name, defaultVal); + } + + /** + * Get an array of long parameters, return an empty array if not found. + * @param request current HTTP request + * @param name the name of the parameter with multiple possible values + */ + public static long[] getLongParameters(HttpServletRequest request, String name) { + return ServletRequestUtils.getLongParameters(request, name); + } + + /** + * Get a long parameter, throwing an exception if it isn't found or isn't a number. + * @param request current HTTP request + * @param name the name of the parameter + * @throws ServletRequestBindingException a subclass of ServletException, + * so it doesn't need to be caught + */ + public static long getRequiredLongParameter(HttpServletRequest request, String name) + throws ServletRequestBindingException { + + return ServletRequestUtils.getRequiredLongParameter(request, name); + } + + /** + * Get an array of long parameters, throwing an exception if not found or one is not a number. + * @param request current HTTP request + * @param name the name of the parameter with multiple possible values + * @throws ServletRequestBindingException a subclass of ServletException, + * so it doesn't need to be caught + */ + public static long[] getRequiredLongParameters(HttpServletRequest request, String name) + throws ServletRequestBindingException { + + return ServletRequestUtils.getRequiredLongParameters(request, name); + } + + + /** + * Get a Float parameter, or null if not present. + * Throws an exception if it the parameter value isn't a number. + * @param request current HTTP request + * @param name the name of the parameter + * @return the Float value, or null if not present + * @throws ServletRequestBindingException a subclass of ServletException, + * so it doesn't need to be caught + */ + public static Float getFloatParameter(HttpServletRequest request, String name) + throws ServletRequestBindingException { + + return ServletRequestUtils.getFloatParameter(request, name); + } + + /** + * Get a float parameter, with a fallback value. Never throws an exception. + * Can pass a distinguished value as default to enable checks of whether it was supplied. + * @param request current HTTP request + * @param name the name of the parameter + * @param defaultVal the default value to use as fallback + */ + public static float getFloatParameter(HttpServletRequest request, String name, float defaultVal) { + return ServletRequestUtils.getFloatParameter(request, name, defaultVal); + } + + /** + * Get an array of float parameters, return an empty array if not found. + * @param request current HTTP request + * @param name the name of the parameter with multiple possible values + */ + public static float[] getFloatParameters(HttpServletRequest request, String name) { + return ServletRequestUtils.getFloatParameters(request, name); + } + + /** + * Get a float parameter, throwing an exception if it isn't found or isn't a number. + * @param request current HTTP request + * @param name the name of the parameter + * @throws ServletRequestBindingException a subclass of ServletException, + * so it doesn't need to be caught + */ + public static float getRequiredFloatParameter(HttpServletRequest request, String name) + throws ServletRequestBindingException { + + return ServletRequestUtils.getRequiredFloatParameter(request, name); + } + + /** + * Get an array of float parameters, throwing an exception if not found or one is not a number. + * @param request current HTTP request + * @param name the name of the parameter with multiple possible values + * @throws ServletRequestBindingException a subclass of ServletException, + * so it doesn't need to be caught + */ + public static float[] getRequiredFloatParameters(HttpServletRequest request, String name) + throws ServletRequestBindingException { + + return ServletRequestUtils.getRequiredFloatParameters(request, name); + } + + + /** + * Get a Double parameter, or null if not present. + * Throws an exception if it the parameter value isn't a number. + * @param request current HTTP request + * @param name the name of the parameter + * @return the Double value, or null if not present + * @throws ServletRequestBindingException a subclass of ServletException, + * so it doesn't need to be caught + */ + public static Double getDoubleParameter(HttpServletRequest request, String name) + throws ServletRequestBindingException { + + return ServletRequestUtils.getDoubleParameter(request, name); + } + + /** + * Get a double parameter, with a fallback value. Never throws an exception. + * Can pass a distinguished value as default to enable checks of whether it was supplied. + * @param request current HTTP request + * @param name the name of the parameter + * @param defaultVal the default value to use as fallback + */ + public static double getDoubleParameter(HttpServletRequest request, String name, double defaultVal) { + return ServletRequestUtils.getDoubleParameter(request, name, defaultVal); + } + + /** + * Get an array of double parameters, return an empty array if not found. + * @param request current HTTP request + * @param name the name of the parameter with multiple possible values + */ + public static double[] getDoubleParameters(HttpServletRequest request, String name) { + return ServletRequestUtils.getDoubleParameters(request, name); + } + + /** + * Get a double parameter, throwing an exception if it isn't found or isn't a number. + * @param request current HTTP request + * @param name the name of the parameter + * @throws ServletRequestBindingException a subclass of ServletException, + * so it doesn't need to be caught + */ + public static double getRequiredDoubleParameter(HttpServletRequest request, String name) + throws ServletRequestBindingException { + + return ServletRequestUtils.getRequiredDoubleParameter(request, name); + } + + /** + * Get an array of double parameters, throwing an exception if not found or one is not a number. + * @param request current HTTP request + * @param name the name of the parameter with multiple possible values + * @throws ServletRequestBindingException a subclass of ServletException, + * so it doesn't need to be caught + */ + public static double[] getRequiredDoubleParameters(HttpServletRequest request, String name) + throws ServletRequestBindingException { + + return ServletRequestUtils.getRequiredDoubleParameters(request, name); + } + + + /** + * Get a Boolean parameter, or null if not present. + * Throws an exception if it the parameter value isn't a boolean. + *

Accepts "true", "on", "yes" (any case) and "1" as values for true; + * treats every other non-empty value as false (i.e. parses leniently). + * @param request current HTTP request + * @param name the name of the parameter + * @return the Boolean value, or null if not present + * @throws ServletRequestBindingException a subclass of ServletException, + * so it doesn't need to be caught + */ + public static Boolean getBooleanParameter(HttpServletRequest request, String name) + throws ServletRequestBindingException { + + if (request.getParameter(name) == null) { + return null; + } + return (getRequiredBooleanParameter(request, name) ? Boolean.TRUE : Boolean.FALSE); + } + + /** + * Get a boolean parameter, with a fallback value. Never throws an exception. + * Can pass a distinguished value as default to enable checks of whether it was supplied. + *

Accepts "true", "on", "yes" (any case) and "1" as values for true; + * treats every other non-empty value as false (i.e. parses leniently). + * @param request current HTTP request + * @param name the name of the parameter + * @param defaultVal the default value to use as fallback + */ + public static boolean getBooleanParameter(HttpServletRequest request, String name, boolean defaultVal) { + if (request.getParameter(name) == null) { + return defaultVal; + } + try { + return getRequiredBooleanParameter(request, name); + } + catch (ServletRequestBindingException ex) { + return defaultVal; + } + } + + /** + * Get an array of boolean parameters, return an empty array if not found. + *

Accepts "true", "on", "yes" (any case) and "1" as values for true; + * treats every other non-empty value as false (i.e. parses leniently). + * @param request current HTTP request + * @param name the name of the parameter with multiple possible values + */ + public static boolean[] getBooleanParameters(HttpServletRequest request, String name) { + try { + return getRequiredBooleanParameters(request, name); + } + catch (ServletRequestBindingException ex) { + return new boolean[0]; + } + } + + /** + * Get a boolean parameter, throwing an exception if it isn't found + * or isn't a boolean. + *

Accepts "true", "on", "yes" (any case) and "1" as values for true; + * treats every other non-empty value as false (i.e. parses leniently). + * @param request current HTTP request + * @param name the name of the parameter + * @throws ServletRequestBindingException a subclass of ServletException, + * so it doesn't need to be caught + */ + public static boolean getRequiredBooleanParameter(HttpServletRequest request, String name) + throws ServletRequestBindingException { + + boolean value = ServletRequestUtils.getRequiredBooleanParameter(request, name); + if (!value && "".equals(request.getParameter(name))) { + throw new ServletRequestBindingException( + "Required boolean parameter '" + name + "' contains no value"); + } + return value; + } + + /** + * Get an array of boolean parameters, throwing an exception if not found + * or one isn't a boolean. + *

Accepts "true", "on", "yes" (any case) and "1" as values for true; + * treats every other non-empty value as false (i.e. parses leniently). + * @param request current HTTP request + * @param name the name of the parameter + * @throws ServletRequestBindingException a subclass of ServletException, + * so it doesn't need to be caught + */ + public static boolean[] getRequiredBooleanParameters(HttpServletRequest request, String name) + throws ServletRequestBindingException { + + boolean[] values = ServletRequestUtils.getRequiredBooleanParameters(request, name); + for (int i = 0; i < values.length; i++) { + if (!values[i] && "".equals(request.getParameterValues(name)[i])) { + throw new ServletRequestBindingException( + "Required boolean parameter '" + name + "' contains no value"); + } + } + return values; + } + + + /** + * Get a String parameter, or null if not present. + * Throws an exception if it the parameter value is empty. + * @param request current HTTP request + * @param name the name of the parameter + * @return the String value, or null if not present + * @throws ServletRequestBindingException a subclass of ServletException, + * so it doesn't need to be caught + */ + public static String getStringParameter(HttpServletRequest request, String name) + throws ServletRequestBindingException { + + if (request.getParameter(name) == null) { + return null; + } + return getRequiredStringParameter(request, name); + } + + /** + * Get a String parameter, with a fallback value. Never throws an exception. + * Can pass a distinguished value to default to enable checks of whether it was supplied. + * @param request current HTTP request + * @param name the name of the parameter + * @param defaultVal the default value to use as fallback + */ + public static String getStringParameter(HttpServletRequest request, String name, String defaultVal) { + if (request.getParameter(name) == null) { + return defaultVal; + } + try { + return getRequiredStringParameter(request, name); + } + catch (ServletRequestBindingException ex) { + return defaultVal; + } + } + + /** + * Get an array of String parameters, return an empty array if not found. + * @param request current HTTP request + * @param name the name of the parameter with multiple possible values + */ + public static String[] getStringParameters(HttpServletRequest request, String name) { + try { + return getRequiredStringParameters(request, name); + } + catch (ServletRequestBindingException ex) { + return new String[0]; + } + } + + /** + * Get a String parameter, throwing an exception if it isn't found or is empty. + * @param request current HTTP request + * @param name the name of the parameter + * @throws ServletRequestBindingException a subclass of ServletException, + * so it doesn't need to be caught + */ + public static String getRequiredStringParameter(HttpServletRequest request, String name) + throws ServletRequestBindingException { + + String value = ServletRequestUtils.getRequiredStringParameter(request, name); + if ("".equals(value)) { + throw new ServletRequestBindingException( + "Required string parameter '" + name + "' contains no value"); + } + return value; + } + + /** + * Get an array of String parameters, throwing an exception if not found or one is empty. + * @param request current HTTP request + * @param name the name of the parameter + * @throws ServletRequestBindingException a subclass of ServletException, + * so it doesn't need to be caught + */ + public static String[] getRequiredStringParameters(HttpServletRequest request, String name) + throws ServletRequestBindingException { + + String[] values = ServletRequestUtils.getRequiredStringParameters(request, name); + for (int i = 0; i < values.length; i++) { + if ("".equals(values[i])) { + throw new ServletRequestBindingException( + "Required string parameter '" + name + "' contains no value"); + } + } + return values; + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/ServletRequestBindingException.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/ServletRequestBindingException.java new file mode 100644 index 00000000000..8165950107e --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/ServletRequestBindingException.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2006 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.web.bind; + +import org.springframework.web.util.NestedServletException; + +/** + * Fatal binding exception, thrown when we want to + * treat binding exceptions as unrecoverable. + * + *

Extends ServletException for convenient throwing in any Servlet resource + * (such as a Filter), and NestedServletException for proper root cause handling + * (as the plain ServletException doesn't expose its root cause at all). + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +public class ServletRequestBindingException extends NestedServletException { + + /** + * Constructor for ServletRequestBindingException. + * @param msg the detail message + */ + public ServletRequestBindingException(String msg) { + super(msg); + } + + /** + * Constructor for ServletRequestBindingException. + * @param msg the detail message + * @param cause the root cause + */ + public ServletRequestBindingException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java new file mode 100644 index 00000000000..5bc4cd7b622 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java @@ -0,0 +1,127 @@ +/* + * Copyright 2002-2008 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.web.bind; + +import javax.servlet.ServletRequest; + +import org.springframework.beans.MutablePropertyValues; +import org.springframework.validation.BindException; +import org.springframework.web.multipart.MultipartHttpServletRequest; + +/** + * Special {@link org.springframework.validation.DataBinder} to perform data binding + * from servlet request parameters to JavaBeans, including support for multipart files. + * + *

See the DataBinder/WebDataBinder superclasses for customization options, + * which include specifying allowed/required fields, and registering custom + * property editors. + * + *

Used by Spring Web MVC's BaseCommandController and MultiActionController. + * Note that BaseCommandController and its subclasses allow for easy customization + * of the binder instances that they use through overriding initBinder. + * + *

Can also be used for manual data binding in custom web controllers: + * for example, in a plain Controller implementation or in a MultiActionController + * handler method. Simply instantiate a ServletRequestDataBinder for each binding + * process, and invoke bind with the current ServletRequest as argument: + * + *

+ * MyBean myBean = new MyBean();
+ * // apply binder to custom target object
+ * ServletRequestDataBinder binder = new ServletRequestDataBinder(myBean);
+ * // register custom editors, if desired
+ * binder.registerCustomEditor(...);
+ * // trigger actual binding of request parameters
+ * binder.bind(request);
+ * // optionally evaluate binding errors
+ * Errors errors = binder.getErrors();
+ * ...
+ * + * @author Rod Johnson + * @author Juergen Hoeller + * @see #bind(javax.servlet.ServletRequest) + * @see #registerCustomEditor + * @see #setAllowedFields + * @see #setRequiredFields + * @see #setFieldMarkerPrefix + * @see org.springframework.web.servlet.mvc.BaseCommandController#initBinder + */ +public class ServletRequestDataBinder extends WebDataBinder { + + /** + * Create a new ServletRequestDataBinder instance, with default object name. + * @param target the target object to bind onto (or null + * if the binder is just used to convert a plain parameter value) + * @see #DEFAULT_OBJECT_NAME + */ + public ServletRequestDataBinder(Object target) { + super(target); + } + + /** + * Create a new ServletRequestDataBinder instance. + * @param target the target object to bind onto (or null + * if the binder is just used to convert a plain parameter value) + * @param objectName the name of the target object + */ + public ServletRequestDataBinder(Object target, String objectName) { + super(target, objectName); + } + + + /** + * Bind the parameters of the given request to this binder's target, + * also binding multipart files in case of a multipart request. + *

This call can create field errors, representing basic binding + * errors like a required field (code "required"), or type mismatch + * between value and bean property (code "typeMismatch"). + *

Multipart files are bound via their parameter name, just like normal + * HTTP parameters: i.e. "uploadedFile" to an "uploadedFile" bean property, + * invoking a "setUploadedFile" setter method. + *

The type of the target property for a multipart file can be MultipartFile, + * byte[], or String. The latter two receive the contents of the uploaded file; + * all metadata like original file name, content type, etc are lost in those cases. + * @param request request with parameters to bind (can be multipart) + * @see org.springframework.web.multipart.MultipartHttpServletRequest + * @see org.springframework.web.multipart.MultipartFile + * @see #bindMultipartFiles + * @see #bind(org.springframework.beans.PropertyValues) + */ + public void bind(ServletRequest request) { + MutablePropertyValues mpvs = new ServletRequestParameterPropertyValues(request); + if (request instanceof MultipartHttpServletRequest) { + MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request; + bindMultipartFiles(multipartRequest.getFileMap(), mpvs); + } + doBind(mpvs); + } + + /** + * Treats errors as fatal. + *

Use this method only if it's an error if the input isn't valid. + * This might be appropriate if all input is from dropdowns, for example. + * @throws ServletRequestBindingException subclass of ServletException on any binding problem + */ + public void closeNoCatch() throws ServletRequestBindingException { + if (getBindingResult().hasErrors()) { + throw new ServletRequestBindingException( + "Errors binding onto object '" + getBindingResult().getObjectName() + "'", + new BindException(getBindingResult())); + } + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/ServletRequestParameterPropertyValues.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/ServletRequestParameterPropertyValues.java new file mode 100644 index 00000000000..a5e36bb89df --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/ServletRequestParameterPropertyValues.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2005 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.web.bind; + +import javax.servlet.ServletRequest; + +import org.springframework.beans.MutablePropertyValues; +import org.springframework.web.util.WebUtils; + +/** + * PropertyValues implementation created from parameters in a ServletRequest. + * Can look for all property values beginning with a certain prefix and + * prefix separator (default is "_"). + * + *

For example, with a prefix of "spring", "spring_param1" and + * "spring_param2" result in a Map with "param1" and "param2" as keys. + * + *

This class is not immutable to be able to efficiently remove property + * values that should be ignored for binding. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see org.springframework.web.util.WebUtils#getParametersStartingWith + */ +public class ServletRequestParameterPropertyValues extends MutablePropertyValues { + + /** Default prefix separator */ + public static final String DEFAULT_PREFIX_SEPARATOR = "_"; + + + /** + * Create new ServletRequestPropertyValues using no prefix + * (and hence, no prefix separator). + * @param request HTTP request + */ + public ServletRequestParameterPropertyValues(ServletRequest request) { + this(request, null, null); + } + + /** + * Create new ServletRequestPropertyValues using the given prefix and + * the default prefix separator (the underscore character "_"). + * @param request HTTP request + * @param prefix the prefix for parameters (the full prefix will + * consist of this plus the separator) + * @see #DEFAULT_PREFIX_SEPARATOR + */ + public ServletRequestParameterPropertyValues(ServletRequest request, String prefix) { + this(request, prefix, DEFAULT_PREFIX_SEPARATOR); + } + + /** + * Create new ServletRequestPropertyValues supplying both prefix and + * prefix separator. + * @param request HTTP request + * @param prefix the prefix for parameters (the full prefix will + * consist of this plus the separator) + * @param prefixSeparator separator delimiting prefix (e.g. "spring") + * and the rest of the parameter name ("param1", "param2") + */ + public ServletRequestParameterPropertyValues(ServletRequest request, String prefix, String prefixSeparator) { + super(WebUtils.getParametersStartingWith( + request, (prefix != null) ? prefix + prefixSeparator : null)); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/ServletRequestUtils.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/ServletRequestUtils.java new file mode 100644 index 00000000000..b6e8be351eb --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/ServletRequestUtils.java @@ -0,0 +1,697 @@ +/* + * Copyright 2002-2007 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.web.bind; + +import javax.servlet.ServletRequest; + +/** + * Parameter extraction methods, for an approach distinct from data binding, + * in which parameters of specific types are required. + * + *

This approach is very useful for simple submissions, where binding + * request parameters to a command object would be overkill. + * + * @author Juergen Hoeller + * @author Keith Donald + * @since 2.0 + */ +public abstract class ServletRequestUtils { + + private static final IntParser INT_PARSER = new IntParser(); + + private static final LongParser LONG_PARSER = new LongParser(); + + private static final FloatParser FLOAT_PARSER = new FloatParser(); + + private static final DoubleParser DOUBLE_PARSER = new DoubleParser(); + + private static final BooleanParser BOOLEAN_PARSER = new BooleanParser(); + + private static final StringParser STRING_PARSER = new StringParser(); + + + /** + * Get an Integer parameter, or null if not present. + * Throws an exception if it the parameter value isn't a number. + * @param request current HTTP request + * @param name the name of the parameter + * @return the Integer value, or null if not present + * @throws ServletRequestBindingException a subclass of ServletException, + * so it doesn't need to be caught + */ + public static Integer getIntParameter(ServletRequest request, String name) + throws ServletRequestBindingException { + + if (request.getParameter(name) == null) { + return null; + } + return new Integer(getRequiredIntParameter(request, name)); + } + + /** + * Get an int parameter, with a fallback value. Never throws an exception. + * Can pass a distinguished value as default to enable checks of whether it was supplied. + * @param request current HTTP request + * @param name the name of the parameter + * @param defaultVal the default value to use as fallback + */ + public static int getIntParameter(ServletRequest request, String name, int defaultVal) { + if (request.getParameter(name) == null) { + return defaultVal; + } + try { + return getRequiredIntParameter(request, name); + } + catch (ServletRequestBindingException ex) { + return defaultVal; + } + } + + /** + * Get an array of int parameters, return an empty array if not found. + * @param request current HTTP request + * @param name the name of the parameter with multiple possible values + */ + public static int[] getIntParameters(ServletRequest request, String name) { + try { + return getRequiredIntParameters(request, name); + } + catch (ServletRequestBindingException ex) { + return new int[0]; + } + } + + /** + * Get an int parameter, throwing an exception if it isn't found or isn't a number. + * @param request current HTTP request + * @param name the name of the parameter + * @throws ServletRequestBindingException a subclass of ServletException, + * so it doesn't need to be caught + */ + public static int getRequiredIntParameter(ServletRequest request, String name) + throws ServletRequestBindingException { + + return INT_PARSER.parseInt(name, request.getParameter(name)); + } + + /** + * Get an array of int parameters, throwing an exception if not found or one is not a number.. + * @param request current HTTP request + * @param name the name of the parameter with multiple possible values + * @throws ServletRequestBindingException a subclass of ServletException, + * so it doesn't need to be caught + */ + public static int[] getRequiredIntParameters(ServletRequest request, String name) + throws ServletRequestBindingException { + + return INT_PARSER.parseInts(name, request.getParameterValues(name)); + } + + + /** + * Get a Long parameter, or null if not present. + * Throws an exception if it the parameter value isn't a number. + * @param request current HTTP request + * @param name the name of the parameter + * @return the Long value, or null if not present + * @throws ServletRequestBindingException a subclass of ServletException, + * so it doesn't need to be caught + */ + public static Long getLongParameter(ServletRequest request, String name) + throws ServletRequestBindingException { + + if (request.getParameter(name) == null) { + return null; + } + return new Long(getRequiredLongParameter(request, name)); + } + + /** + * Get a long parameter, with a fallback value. Never throws an exception. + * Can pass a distinguished value as default to enable checks of whether it was supplied. + * @param request current HTTP request + * @param name the name of the parameter + * @param defaultVal the default value to use as fallback + */ + public static long getLongParameter(ServletRequest request, String name, long defaultVal) { + if (request.getParameter(name) == null) { + return defaultVal; + } + try { + return getRequiredLongParameter(request, name); + } + catch (ServletRequestBindingException ex) { + return defaultVal; + } + } + + /** + * Get an array of long parameters, return an empty array if not found. + * @param request current HTTP request + * @param name the name of the parameter with multiple possible values + */ + public static long[] getLongParameters(ServletRequest request, String name) { + try { + return getRequiredLongParameters(request, name); + } + catch (ServletRequestBindingException ex) { + return new long[0]; + } + } + + /** + * Get a long parameter, throwing an exception if it isn't found or isn't a number. + * @param request current HTTP request + * @param name the name of the parameter + * @throws ServletRequestBindingException a subclass of ServletException, + * so it doesn't need to be caught + */ + public static long getRequiredLongParameter(ServletRequest request, String name) + throws ServletRequestBindingException { + + return LONG_PARSER.parseLong(name, request.getParameter(name)); + } + + /** + * Get an array of long parameters, throwing an exception if not found or one is not a number. + * @param request current HTTP request + * @param name the name of the parameter with multiple possible values + * @throws ServletRequestBindingException a subclass of ServletException, + * so it doesn't need to be caught + */ + public static long[] getRequiredLongParameters(ServletRequest request, String name) + throws ServletRequestBindingException { + + return LONG_PARSER.parseLongs(name, request.getParameterValues(name)); + } + + + /** + * Get a Float parameter, or null if not present. + * Throws an exception if it the parameter value isn't a number. + * @param request current HTTP request + * @param name the name of the parameter + * @return the Float value, or null if not present + * @throws ServletRequestBindingException a subclass of ServletException, + * so it doesn't need to be caught + */ + public static Float getFloatParameter(ServletRequest request, String name) + throws ServletRequestBindingException { + + if (request.getParameter(name) == null) { + return null; + } + return new Float(getRequiredFloatParameter(request, name)); + } + + /** + * Get a float parameter, with a fallback value. Never throws an exception. + * Can pass a distinguished value as default to enable checks of whether it was supplied. + * @param request current HTTP request + * @param name the name of the parameter + * @param defaultVal the default value to use as fallback + */ + public static float getFloatParameter(ServletRequest request, String name, float defaultVal) { + if (request.getParameter(name) == null) { + return defaultVal; + } + try { + return getRequiredFloatParameter(request, name); + } + catch (ServletRequestBindingException ex) { + return defaultVal; + } + } + + /** + * Get an array of float parameters, return an empty array if not found. + * @param request current HTTP request + * @param name the name of the parameter with multiple possible values + */ + public static float[] getFloatParameters(ServletRequest request, String name) { + try { + return getRequiredFloatParameters(request, name); + } + catch (ServletRequestBindingException ex) { + return new float[0]; + } + } + + /** + * Get a float parameter, throwing an exception if it isn't found or isn't a number. + * @param request current HTTP request + * @param name the name of the parameter + * @throws ServletRequestBindingException a subclass of ServletException, + * so it doesn't need to be caught + */ + public static float getRequiredFloatParameter(ServletRequest request, String name) + throws ServletRequestBindingException { + + return FLOAT_PARSER.parseFloat(name, request.getParameter(name)); + } + + /** + * Get an array of float parameters, throwing an exception if not found or one is not a number. + * @param request current HTTP request + * @param name the name of the parameter with multiple possible values + * @throws ServletRequestBindingException a subclass of ServletException, + * so it doesn't need to be caught + */ + public static float[] getRequiredFloatParameters(ServletRequest request, String name) + throws ServletRequestBindingException { + + return FLOAT_PARSER.parseFloats(name, request.getParameterValues(name)); + } + + + /** + * Get a Double parameter, or null if not present. + * Throws an exception if it the parameter value isn't a number. + * @param request current HTTP request + * @param name the name of the parameter + * @return the Double value, or null if not present + * @throws ServletRequestBindingException a subclass of ServletException, + * so it doesn't need to be caught + */ + public static Double getDoubleParameter(ServletRequest request, String name) + throws ServletRequestBindingException { + + if (request.getParameter(name) == null) { + return null; + } + return new Double(getRequiredDoubleParameter(request, name)); + } + + /** + * Get a double parameter, with a fallback value. Never throws an exception. + * Can pass a distinguished value as default to enable checks of whether it was supplied. + * @param request current HTTP request + * @param name the name of the parameter + * @param defaultVal the default value to use as fallback + */ + public static double getDoubleParameter(ServletRequest request, String name, double defaultVal) { + if (request.getParameter(name) == null) { + return defaultVal; + } + try { + return getRequiredDoubleParameter(request, name); + } + catch (ServletRequestBindingException ex) { + return defaultVal; + } + } + + /** + * Get an array of double parameters, return an empty array if not found. + * @param request current HTTP request + * @param name the name of the parameter with multiple possible values + */ + public static double[] getDoubleParameters(ServletRequest request, String name) { + try { + return getRequiredDoubleParameters(request, name); + } + catch (ServletRequestBindingException ex) { + return new double[0]; + } + } + + /** + * Get a double parameter, throwing an exception if it isn't found or isn't a number. + * @param request current HTTP request + * @param name the name of the parameter + * @throws ServletRequestBindingException a subclass of ServletException, + * so it doesn't need to be caught + */ + public static double getRequiredDoubleParameter(ServletRequest request, String name) + throws ServletRequestBindingException { + + return DOUBLE_PARSER.parseDouble(name, request.getParameter(name)); + } + + /** + * Get an array of double parameters, throwing an exception if not found or one is not a number. + * @param request current HTTP request + * @param name the name of the parameter with multiple possible values + * @throws ServletRequestBindingException a subclass of ServletException, + * so it doesn't need to be caught + */ + public static double[] getRequiredDoubleParameters(ServletRequest request, String name) + throws ServletRequestBindingException { + + return DOUBLE_PARSER.parseDoubles(name, request.getParameterValues(name)); + } + + + /** + * Get a Boolean parameter, or null if not present. + * Throws an exception if it the parameter value isn't a boolean. + *

Accepts "true", "on", "yes" (any case) and "1" as values for true; + * treats every other non-empty value as false (i.e. parses leniently). + * @param request current HTTP request + * @param name the name of the parameter + * @return the Boolean value, or null if not present + * @throws ServletRequestBindingException a subclass of ServletException, + * so it doesn't need to be caught + */ + public static Boolean getBooleanParameter(ServletRequest request, String name) + throws ServletRequestBindingException { + + if (request.getParameter(name) == null) { + return null; + } + return (getRequiredBooleanParameter(request, name) ? Boolean.TRUE : Boolean.FALSE); + } + + /** + * Get a boolean parameter, with a fallback value. Never throws an exception. + * Can pass a distinguished value as default to enable checks of whether it was supplied. + *

Accepts "true", "on", "yes" (any case) and "1" as values for true; + * treats every other non-empty value as false (i.e. parses leniently). + * @param request current HTTP request + * @param name the name of the parameter + * @param defaultVal the default value to use as fallback + */ + public static boolean getBooleanParameter(ServletRequest request, String name, boolean defaultVal) { + if (request.getParameter(name) == null) { + return defaultVal; + } + try { + return getRequiredBooleanParameter(request, name); + } + catch (ServletRequestBindingException ex) { + return defaultVal; + } + } + + /** + * Get an array of boolean parameters, return an empty array if not found. + *

Accepts "true", "on", "yes" (any case) and "1" as values for true; + * treats every other non-empty value as false (i.e. parses leniently). + * @param request current HTTP request + * @param name the name of the parameter with multiple possible values + */ + public static boolean[] getBooleanParameters(ServletRequest request, String name) { + try { + return getRequiredBooleanParameters(request, name); + } + catch (ServletRequestBindingException ex) { + return new boolean[0]; + } + } + + /** + * Get a boolean parameter, throwing an exception if it isn't found + * or isn't a boolean. + *

Accepts "true", "on", "yes" (any case) and "1" as values for true; + * treats every other non-empty value as false (i.e. parses leniently). + * @param request current HTTP request + * @param name the name of the parameter + * @throws ServletRequestBindingException a subclass of ServletException, + * so it doesn't need to be caught + */ + public static boolean getRequiredBooleanParameter(ServletRequest request, String name) + throws ServletRequestBindingException { + + return BOOLEAN_PARSER.parseBoolean(name, request.getParameter(name)); + } + + /** + * Get an array of boolean parameters, throwing an exception if not found + * or one isn't a boolean. + *

Accepts "true", "on", "yes" (any case) and "1" as values for true; + * treats every other non-empty value as false (i.e. parses leniently). + * @param request current HTTP request + * @param name the name of the parameter + * @throws ServletRequestBindingException a subclass of ServletException, + * so it doesn't need to be caught + */ + public static boolean[] getRequiredBooleanParameters(ServletRequest request, String name) + throws ServletRequestBindingException { + + return BOOLEAN_PARSER.parseBooleans(name, request.getParameterValues(name)); + } + + + /** + * Get a String parameter, or null if not present. + * @param request current HTTP request + * @param name the name of the parameter + * @return the String value, or null if not present + * @throws ServletRequestBindingException a subclass of ServletException, + * so it doesn't need to be caught + */ + public static String getStringParameter(ServletRequest request, String name) + throws ServletRequestBindingException { + + if (request.getParameter(name) == null) { + return null; + } + return getRequiredStringParameter(request, name); + } + + /** + * Get a String parameter, with a fallback value. Never throws an exception. + * Can pass a distinguished value to default to enable checks of whether it was supplied. + * @param request current HTTP request + * @param name the name of the parameter + * @param defaultVal the default value to use as fallback + */ + public static String getStringParameter(ServletRequest request, String name, String defaultVal) { + String val = request.getParameter(name); + return (val != null ? val : defaultVal); + } + + /** + * Get an array of String parameters, return an empty array if not found. + * @param request current HTTP request + * @param name the name of the parameter with multiple possible values + */ + public static String[] getStringParameters(ServletRequest request, String name) { + try { + return getRequiredStringParameters(request, name); + } + catch (ServletRequestBindingException ex) { + return new String[0]; + } + } + + /** + * Get a String parameter, throwing an exception if it isn't found. + * @param request current HTTP request + * @param name the name of the parameter + * @throws ServletRequestBindingException a subclass of ServletException, + * so it doesn't need to be caught + */ + public static String getRequiredStringParameter(ServletRequest request, String name) + throws ServletRequestBindingException { + + return STRING_PARSER.validateRequiredString(name, request.getParameter(name)); + } + + /** + * Get an array of String parameters, throwing an exception if not found. + * @param request current HTTP request + * @param name the name of the parameter + * @throws ServletRequestBindingException a subclass of ServletException, + * so it doesn't need to be caught + */ + public static String[] getRequiredStringParameters(ServletRequest request, String name) + throws ServletRequestBindingException { + + return STRING_PARSER.validateRequiredStrings(name, request.getParameterValues(name)); + } + + + private abstract static class ParameterParser { + + protected final Object parse(String name, String parameter) throws ServletRequestBindingException { + validateRequiredParameter(name, parameter); + try { + return doParse(parameter); + } + catch (NumberFormatException ex) { + throw new ServletRequestBindingException( + "Required " + getType() + " parameter '" + name + "' with value of '" + + parameter + "' is not a valid number", ex); + } + } + + protected final void validateRequiredParameter(String name, Object parameter) + throws ServletRequestBindingException { + + if (parameter == null) { + throw new MissingServletRequestParameterException(name, getType()); + } + } + + protected abstract String getType(); + + protected abstract Object doParse(String parameter) throws NumberFormatException; + } + + + private static class IntParser extends ParameterParser { + + protected String getType() { + return "int"; + } + + protected Object doParse(String s) throws NumberFormatException { + return Integer.valueOf(s); + } + + public int parseInt(String name, String parameter) throws ServletRequestBindingException { + return ((Number) parse(name, parameter)).intValue(); + } + + public int[] parseInts(String name, String[] values) throws ServletRequestBindingException { + validateRequiredParameter(name, values); + int[] parameters = new int[values.length]; + for (int i = 0; i < values.length; i++) { + parameters[i] = parseInt(name, values[i]); + } + return parameters; + } + } + + + private static class LongParser extends ParameterParser { + + protected String getType() { + return "long"; + } + + protected Object doParse(String parameter) throws NumberFormatException { + return Long.valueOf(parameter); + } + + public long parseLong(String name, String parameter) throws ServletRequestBindingException { + return ((Number) parse(name, parameter)).longValue(); + } + + public long[] parseLongs(String name, String[] values) throws ServletRequestBindingException { + validateRequiredParameter(name, values); + long[] parameters = new long[values.length]; + for (int i = 0; i < values.length; i++) { + parameters[i] = parseLong(name, values[i]); + } + return parameters; + } + } + + + private static class FloatParser extends ParameterParser { + + protected String getType() { + return "float"; + } + + protected Object doParse(String parameter) throws NumberFormatException { + return Float.valueOf(parameter); + } + + public float parseFloat(String name, String parameter) throws ServletRequestBindingException { + return ((Number) parse(name, parameter)).floatValue(); + } + + public float[] parseFloats(String name, String[] values) throws ServletRequestBindingException { + validateRequiredParameter(name, values); + float[] parameters = new float[values.length]; + for (int i = 0; i < values.length; i++) { + parameters[i] = parseFloat(name, values[i]); + } + return parameters; + } + } + + + private static class DoubleParser extends ParameterParser { + + protected String getType() { + return "double"; + } + + protected Object doParse(String parameter) throws NumberFormatException { + return Double.valueOf(parameter); + } + + public double parseDouble(String name, String parameter) throws ServletRequestBindingException { + return ((Number) parse(name, parameter)).doubleValue(); + } + + public double[] parseDoubles(String name, String[] values) throws ServletRequestBindingException { + validateRequiredParameter(name, values); + double[] parameters = new double[values.length]; + for (int i = 0; i < values.length; i++) { + parameters[i] = parseDouble(name, values[i]); + } + return parameters; + } + } + + + private static class BooleanParser extends ParameterParser { + + protected String getType() { + return "boolean"; + } + + protected Object doParse(String parameter) throws NumberFormatException { + return (parameter.equalsIgnoreCase("true") || parameter.equalsIgnoreCase("on") || + parameter.equalsIgnoreCase("yes") || parameter.equals("1") ? Boolean.TRUE : Boolean.FALSE); + } + + public boolean parseBoolean(String name, String parameter) throws ServletRequestBindingException { + return ((Boolean) parse(name, parameter)).booleanValue(); + } + + public boolean[] parseBooleans(String name, String[] values) throws ServletRequestBindingException { + validateRequiredParameter(name, values); + boolean[] parameters = new boolean[values.length]; + for (int i = 0; i < values.length; i++) { + parameters[i] = parseBoolean(name, values[i]); + } + return parameters; + } + } + + + private static class StringParser extends ParameterParser { + + protected String getType() { + return "string"; + } + + protected Object doParse(String parameter) throws NumberFormatException { + return parameter; + } + + public String validateRequiredString(String name, String value) throws ServletRequestBindingException { + validateRequiredParameter(name, value); + return value; + } + + public String[] validateRequiredStrings(String name, String[] values) throws ServletRequestBindingException { + validateRequiredParameter(name, values); + for (int i = 0; i < values.length; i++) { + validateRequiredParameter(name, values[i]); + } + return values; + } + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/WebDataBinder.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/WebDataBinder.java new file mode 100644 index 00000000000..bacf8227b71 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/WebDataBinder.java @@ -0,0 +1,227 @@ +/* + * Copyright 2002-2008 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.web.bind; + +import java.lang.reflect.Array; +import java.util.Iterator; +import java.util.Map; + +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.PropertyValue; +import org.springframework.validation.DataBinder; +import org.springframework.web.multipart.MultipartFile; + +/** + * Special {@link DataBinder} for data binding from web request parameters + * to JavaBean objects. Designed for web environments, but not dependent on + * the Servlet API; serves as base class for more specific DataBinder variants, + * such as {@link org.springframework.web.bind.ServletRequestDataBinder}. + * + *

Includes support for field markers which address a common problem with + * HTML checkboxes and select options: detecting that a field was part of + * the form, but did not generate a request parameter because it was empty. + * A field marker allows to detect that state and reset the corresponding + * bean property accordingly. + * + * @author Juergen Hoeller + * @since 1.2 + * @see #registerCustomEditor + * @see #setAllowedFields + * @see #setRequiredFields + * @see #setFieldMarkerPrefix + * @see ServletRequestDataBinder + */ +public class WebDataBinder extends DataBinder { + + /** + * Default prefix that field marker parameters start with, followed by the field + * name: e.g. "_subscribeToNewsletter" for a field "subscribeToNewsletter". + *

Such a marker parameter indicates that the field was visible, that is, + * existed in the form that caused the submission. If no corresponding field + * value parameter was found, the field will be reset. The value of the field + * marker parameter does not matter in this case; an arbitrary value can be used. + * This is particularly useful for HTML checkboxes and select options. + * @see #setFieldMarkerPrefix + */ + public static final String DEFAULT_FIELD_MARKER_PREFIX = "_"; + + + private String fieldMarkerPrefix = DEFAULT_FIELD_MARKER_PREFIX; + + private boolean bindEmptyMultipartFiles = true; + + + /** + * Create a new WebDataBinder instance, with default object name. + * @param target the target object to bind onto (or null + * if the binder is just used to convert a plain parameter value) + * @see #DEFAULT_OBJECT_NAME + */ + public WebDataBinder(Object target) { + super(target); + } + + /** + * Create a new WebDataBinder instance. + * @param target the target object to bind onto (or null + * if the binder is just used to convert a plain parameter value) + * @param objectName the name of the target object + */ + public WebDataBinder(Object target, String objectName) { + super(target, objectName); + } + + + /** + * Specify a prefix that can be used for parameters that mark potentially + * empty fields, having "prefix + field" as name. Such a marker parameter is + * checked by existence: You can send any value for it, for example "visible". + * This is particularly useful for HTML checkboxes and select options. + *

Default is "_", for "_FIELD" parameters (e.g. "_subscribeToNewsletter"). + * Set this to null if you want to turn off the empty field check completely. + *

HTML checkboxes only send a value when they're checked, so it is not + * possible to detect that a formerly checked box has just been unchecked, + * at least not with standard HTML means. + *

One way to address this is to look for a checkbox parameter value if + * you know that the checkbox has been visible in the form, resetting the + * checkbox if no value found. In Spring web MVC, this typically happens + * in a custom onBind implementation. + *

This auto-reset mechanism addresses this deficiency, provided + * that a marker parameter is sent for each checkbox field, like + * "_subscribeToNewsletter" for a "subscribeToNewsletter" field. + * As the marker parameter is sent in any case, the data binder can + * detect an empty field and automatically reset its value. + * @see #DEFAULT_FIELD_MARKER_PREFIX + * @see org.springframework.web.servlet.mvc.BaseCommandController#onBind + */ + public void setFieldMarkerPrefix(String fieldMarkerPrefix) { + this.fieldMarkerPrefix = fieldMarkerPrefix; + } + + /** + * Return the prefix for parameters that mark potentially empty fields. + */ + public String getFieldMarkerPrefix() { + return this.fieldMarkerPrefix; + } + + /** + * Set whether to bind empty MultipartFile parameters. Default is "true". + *

Turn this off if you want to keep an already bound MultipartFile + * when the user resubmits the form without choosing a different file. + * Else, the already bound MultipartFile will be replaced by an empty + * MultipartFile holder. + * @see org.springframework.web.multipart.MultipartFile + */ + public void setBindEmptyMultipartFiles(boolean bindEmptyMultipartFiles) { + this.bindEmptyMultipartFiles = bindEmptyMultipartFiles; + } + + /** + * Return whether to bind empty MultipartFile parameters. + */ + public boolean isBindEmptyMultipartFiles() { + return this.bindEmptyMultipartFiles; + } + + + /** + * This implementation performs a field marker check + * before delegating to the superclass binding process. + * @see #checkFieldMarkers + */ + protected void doBind(MutablePropertyValues mpvs) { + checkFieldMarkers(mpvs); + super.doBind(mpvs); + } + + /** + * Check the given property values for field markers, + * i.e. for fields that start with the field marker prefix. + *

The existence of a field marker indicates that the specified + * field existed in the form. If the property values do not contain + * a corresponding field value, the field will be considered as empty + * and will be reset appropriately. + * @param mpvs the property values to be bound (can be modified) + * @see #getFieldMarkerPrefix + * @see #getEmptyValue(String, Class) + */ + protected void checkFieldMarkers(MutablePropertyValues mpvs) { + if (getFieldMarkerPrefix() != null) { + String fieldMarkerPrefix = getFieldMarkerPrefix(); + PropertyValue[] pvArray = mpvs.getPropertyValues(); + for (int i = 0; i < pvArray.length; i++) { + PropertyValue pv = pvArray[i]; + if (pv.getName().startsWith(fieldMarkerPrefix)) { + String field = pv.getName().substring(fieldMarkerPrefix.length()); + if (getPropertyAccessor().isWritableProperty(field) && !mpvs.contains(field)) { + Class fieldType = getPropertyAccessor().getPropertyType(field); + mpvs.addPropertyValue(field, getEmptyValue(field, fieldType)); + } + mpvs.removePropertyValue(pv); + } + } + } + } + + /** + * Determine an empty value for the specified field. + *

Default implementation returns Boolean.FALSE + * for boolean fields and an empty array of array types. + * Else, null is used as default. + * @param field the name of the field + * @param fieldType the type of the field + * @return the empty value (for most fields: null) + */ + protected Object getEmptyValue(String field, Class fieldType) { + if (fieldType != null && boolean.class.equals(fieldType) || Boolean.class.equals(fieldType)) { + // Special handling of boolean property. + return Boolean.FALSE; + } + else if (fieldType != null && fieldType.isArray()) { + // Special handling of array property. + return Array.newInstance(fieldType.getComponentType(), 0); + } + else { + // Default value: try null. + return null; + } + } + + + /** + * Bind the multipart files contained in the given request, if any + * (in case of a multipart request). + *

Multipart files will only be added to the property values if they + * are not empty or if we're configured to bind empty multipart files too. + * @param multipartFiles Map of field name String to MultipartFile object + * @param mpvs the property values to be bound (can be modified) + * @see org.springframework.web.multipart.MultipartFile + * @see #setBindEmptyMultipartFiles + */ + protected void bindMultipartFiles(Map multipartFiles, MutablePropertyValues mpvs) { + for (Iterator it = multipartFiles.entrySet().iterator(); it.hasNext();) { + Map.Entry entry = (Map.Entry) it.next(); + String key = (String) entry.getKey(); + MultipartFile value = (MultipartFile) entry.getValue(); + if (isBindEmptyMultipartFiles() || !value.isEmpty()) { + mpvs.addPropertyValue(key, value); + } + } + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/InitBinder.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/InitBinder.java new file mode 100644 index 00000000000..9b22b261c4f --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/InitBinder.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2007 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.web.bind.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation that identifies methods which initialize the + * {@link org.springframework.web.bind.WebDataBinder} which + * will be used for populating command and form object arguments + * of annotated handler methods. + * + *

Such init-binder methods support all arguments that {@link RequestMapping} + * supports, except for command/form objects and corresponding validation result + * objects. Init-binder methods must not have a return value; they are usually + * declared as void. + * + *

Typical arguments are {@link org.springframework.web.bind.WebDataBinder} + * in combination with {@link org.springframework.web.context.request.WebRequest} + * or {@link java.util.Locale}, allowing to register context-specific editors. + * + * @author Juergen Hoeller + * @since 2.5 + * @see org.springframework.web.bind.WebDataBinder + * @see org.springframework.web.context.request.WebRequest + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface InitBinder { + + /** + * The names of command/form attributes and/or request parameters + * that this init-binder method is supposed to apply to. + *

Default is to apply to all command/form attributes and all request parameters + * processed by the annotated handler class. Specifying model attribute names or + * request parameter names here restricts the init-binder method to those specific + * attributes/parameters, with different init-binder methods typically applying to + * different groups of attributes or parameters. + */ + String[] value() default {}; + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/ModelAttribute.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/ModelAttribute.java new file mode 100644 index 00000000000..dbd293ff867 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/ModelAttribute.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2007 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.web.bind.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation that binds a method parameter or method return value + * to a named model attribute, exposed to a web view. Supported + * for {@link RequestMapping} annotated handler classes. + * + *

Can be used to expose command objects to a web view, using + * specific attribute names, through annotating corresponding + * parameters of a {@link RequestMapping} annotated handler method). + * + *

Can also be used to expose reference data to a web view + * through annotating accessor methods in a controller class which + * is based on {@link RequestMapping} annotated handler methods, + * with such accessor methods allowed to have any arguments that + * {@link RequestMapping} supports for handler methods, returning + * the model attribute value to expose. + * + * @author Juergen Hoeller + * @since 2.5 + */ +@Target({ElementType.PARAMETER, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ModelAttribute { + + /** + * The name of the model attribute to bind to. + *

The default model attribute name is inferred from the declared + * attribute type (i.e. the method parameter type or method return type), + * based on the non-qualified class name: + * e.g. "orderAddress" for class "mypackage.OrderAddress", + * or "orderAddressList" for "List<mypackage.OrderAddress>". + */ + String value() default ""; + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java new file mode 100644 index 00000000000..75ed5560eeb --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java @@ -0,0 +1,230 @@ +/* + * Copyright 2002-2008 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.web.bind.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation for mapping web requests onto specific handler classes and/or + * handler methods. Provides consistent style between Servlet and Portlet + * environments, with the semantics adapting to the concrete environment. + * + *

NOTE: Method-level mappings are only allowed to narrow the mapping + * expressed at the class level (if any). HTTP paths / portlet modes need to + * uniquely map onto specific handler beans, with any given path / mode only + * allowed to be mapped onto one specific handler bean (not spread across + * multiple handler beans). It is strongly recommended to co-locate related + * handler methods into the same bean. + * + *

Handler methods which are annotated with this annotation are allowed + * to have very flexible signatures. They may have arguments of the following + * types, in arbitrary order (except for validation results, which need to + * follow right after the corresponding command object, if desired): + *

+ * + *

The following return types are supported for handler methods: + *

+ * + *

NOTE: @RequestMapping will only be processed if a + * corresponding HandlerMapping (for type level annotations) + * and/or HandlerAdapter (for method level annotations) is + * present in the dispatcher. This is the case by default in both + * DispatcherServlet and DispatcherPortlet. + * However, if you are defining custom HandlerMappings or + * HandlerAdapters, then you need to make sure that a + * corresponding custom DefaultAnnotationHandlerMapping + * and/or AnnotationMethodHandlerAdapter is defined as well + * - provided that you intend to use @RequestMapping. + * + * @author Juergen Hoeller + * @author Arjen Poutsma + * @author Sam Brannen + * @since 2.5 + * @see RequestParam + * @see ModelAttribute + * @see SessionAttributes + * @see InitBinder + * @see org.springframework.web.context.request.WebRequest + * @see org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping + * @see org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter + * @see org.springframework.web.portlet.mvc.annotation.DefaultAnnotationHandlerMapping + * @see org.springframework.web.portlet.mvc.annotation.AnnotationMethodHandlerAdapter + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RequestMapping { + + /** + * The primary mapping expressed by this annotation. + *

In a Servlet environment: the path mapping URIs (e.g. "/myPath.do"). + * Ant-style path patterns are also supported (e.g. "/myPath/*.do"). + * At the method level, relative paths (e.g. "edit.do") are supported + * within the primary mapping expressed at the type level. + *

In a Portlet environment: the mapped portlet modes + * (i.e. "EDIT", "VIEW", "HELP" or any custom modes). + *

Supported at the type level as well as at the method level! + * When used at the type level, all method-level mappings inherit + * this primary mapping, narrowing it for a specific handler method. + *

In case of Servlet-based handler methods, the method names are + * taken into account for narrowing if no path was specified explicitly, + * according to the specified + * {@link org.springframework.web.servlet.mvc.multiaction.MethodNameResolver} + * (by default an + * {@link org.springframework.web.servlet.mvc.multiaction.InternalPathMethodNameResolver}). + * Note that this only applies in case of ambiguous annotation mappings + * that do not specify a path mapping explicitly. In other words, + * the method name is only used for narrowing among a set of matching + * methods; it does not constitute a primary path mapping itself. + *

If you have a single default method (without explicit path mapping), + * then all requests without a more specific mapped method found will + * be dispatched to it. If you have multiple such default methods, then + * the method name will be taken into account for choosing between them. + */ + String[] value() default {}; + + /** + * The HTTP request methods to map to, narrowing the primary mapping: + * GET, POST, HEAD, OPTIONS, PUT, DELETE, TRACE. + *

Supported at the type level as well as at the method level! + * When used at the type level, all method-level mappings inherit + * this HTTP method restriction (i.e. the type-level restriction + * gets checked before the handler method is even resolved). + *

Currently only supported in Servlet environments! + * To be supported for Portlet 2.0 resource requests in Spring 3.0 as well. + */ + RequestMethod[] method() default {}; + + /** + * The parameters of the mapped request, narrowing the primary mapping. + *

Same format for any environment: a sequence of "myParam=myValue" style + * expressions, with a request only mapped if each such parameter is found + * to have the given value. "myParam" style expressions are also supported, + * with such parameters having to be present in the request (allowed to have + * any value). Finally, "!myParam" style expressions indicate that the + * specified parameter is not supposed to be present in the request. + *

Supported at the type level as well as at the method level! + * When used at the type level, all method-level mappings inherit + * this parameter restriction (i.e. the type-level restriction + * gets checked before the handler method is even resolved). + *

In a Servlet environment, parameter mappings are considered as restrictions + * that are enforced at the type level. The primary path mapping (i.e. the + * specified URI value) still has to uniquely identify the target handler, with + * parameter mappings simply expressing preconditions for invoking the handler. + *

In a Portlet environment, parameters are taken into account as mapping + * differentiators, i.e. the primary portlet mode mapping plus the parameter + * conditions uniquely identify the target handler. Different handlers may be + * mapped onto the same portlet mode, as long as their parameter mappings differ. + */ + String[] params() default {}; + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/RequestMethod.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/RequestMethod.java new file mode 100644 index 00000000000..91218e4f817 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/RequestMethod.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2008 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.web.bind.annotation; + +/** + * Java 5 enumeration of HTTP request methods. Intended for use + * with the {@link RequestMapping#method()} attribute of the + * {@link RequestMapping} annotation. + * + *

Note that, by default, {@link org.springframework.web.servlet.DispatcherServlet} + * supports GET, HEAD, POST, PUT and DELETE only. DispatcherServlet will + * process TRACE and OPTIONS with the default HttpServlet behavior unless + * explicitly told to dispatch those request types as well: Check out + * the "dispatchOptionsRequest" and "dispatchTraceRequest" properties, + * switching them to "true" if necessary. + * + * @author Juergen Hoeller + * @since 2.5 + * @see RequestMapping + * @see org.springframework.web.servlet.DispatcherServlet#setDispatchOptionsRequest + * @see org.springframework.web.servlet.DispatcherServlet#setDispatchTraceRequest + */ +public enum RequestMethod { + + GET, HEAD, POST, PUT, DELETE, OPTIONS, TRACE; + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/RequestParam.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/RequestParam.java new file mode 100644 index 00000000000..1f7ea2e9cf1 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/RequestParam.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2007 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.web.bind.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation which indicates that a method parameter should be bound + * to a web request parameter. Supported for {@link RequestMapping} + * annotated handler methods in Servlet and Portlet environments. + * + * @author Arjen Poutsma + * @author Juergen Hoeller + * @since 2.5 + * @see RequestMapping + * @see org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter + * @see org.springframework.web.portlet.mvc.annotation.AnnotationMethodHandlerAdapter + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RequestParam { + + /** + * The request parameter to bind to. + */ + String value() default ""; + + /** + * Whether the parameter is required. + *

Default is true, leading to an exception thrown in case + * of the parameter missing in the request. Switch this to false + * if you prefer a null in case of the parameter missing. + */ + boolean required() default true; + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/SessionAttributes.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/SessionAttributes.java new file mode 100644 index 00000000000..076bec779be --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/SessionAttributes.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2008 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.web.bind.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation that indicates the session attributes that a specific handler + * uses. This will typically list the names of model attributes which should be + * transparently stored in the session or some conversational storage, + * serving as form-backing beans. Declared at the type level, applying + * to the model attributes that the annotated handler class operates on. + * + *

NOTE: Session attributes as indicated using this annotation + * correspond to a specific handler's model attributes, getting transparently + * stored in a conversational session. Those attributes will be removed once + * the handler indicates completion of its conversational session. Therefore, + * use this facility for such conversational attributes which are supposed + * to be stored in the session temporarily during the course of a + * specific handler's conversation. + * + *

For permanent session attributes, e.g. a user authentication object, + * use the traditional session.setAttribute method instead. + * Alternatively, consider using the attribute management capabilities of the + * generic {@link org.springframework.web.context.request.WebRequest} interface. + * + * @author Juergen Hoeller + * @since 2.5 + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface SessionAttributes { + + /** + * The names of session attributes in the model, to be stored in the + * session or some conversational storage. + *

Note: This indicates the model attribute names. The session attribute + * names may or may not match the model attribute names; applications should + * not rely on the session attribute names but rather operate on the model only. + */ + String[] value() default {}; + + /** + * The types of session attributes in the model, to be stored in the + * session or some conversational storage. All model attributes of this + * type will be stored in the session, regardless of attribute name. + */ + Class[] types() default {}; + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/package.html b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/package.html new file mode 100644 index 00000000000..fc2b6efadbf --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/package.html @@ -0,0 +1,8 @@ + + + +Annotations for binding requests to controllers and handler methods +as well as for binding request parameters to method arguments. + + + diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvocationException.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvocationException.java new file mode 100644 index 00000000000..bfbb1ac5303 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvocationException.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2008 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.web.bind.annotation.support; + +import java.lang.reflect.Method; + +import org.springframework.core.NestedRuntimeException; + +/** + * Exception indicating that the execution of an annotated MVC handler method failed. + * + * @author Juergen Hoeller + * @since 2.5.6 + * @see HandlerMethodInvoker#invokeHandlerMethod + */ +public class HandlerMethodInvocationException extends NestedRuntimeException { + + /** + * Create a new HandlerMethodInvocationException for the given Method handle and cause. + * @param handlerMethod the handler method handle + * @param cause the cause of the invocation failure + */ + public HandlerMethodInvocationException(Method handlerMethod, Throwable cause) { + super("Failed to invoke handler method [" + handlerMethod + "]", cause); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java new file mode 100644 index 00000000000..4e61fb2c46f --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java @@ -0,0 +1,487 @@ +/* + * Copyright 2002-2008 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.web.bind.annotation.support; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.BeanUtils; +import org.springframework.core.BridgeMethodResolver; +import org.springframework.core.Conventions; +import org.springframework.core.GenericTypeResolver; +import org.springframework.core.MethodParameter; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.ui.ExtendedModelMap; +import org.springframework.ui.Model; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.validation.BindingResult; +import org.springframework.validation.Errors; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.InitBinder; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.support.DefaultSessionAttributeStore; +import org.springframework.web.bind.support.SessionAttributeStore; +import org.springframework.web.bind.support.SessionStatus; +import org.springframework.web.bind.support.SimpleSessionStatus; +import org.springframework.web.bind.support.WebArgumentResolver; +import org.springframework.web.bind.support.WebBindingInitializer; +import org.springframework.web.bind.support.WebRequestDataBinder; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.multipart.MultipartRequest; + +/** + * Support class for invoking an annotated handler method. + * Operates on the introspection results of a {@link HandlerMethodResolver} + * for a specific handler type. + * + *

Used by {@link org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter} + * and {@link org.springframework.web.portlet.mvc.annotation.AnnotationMethodHandlerAdapter}. + * + * @author Juergen Hoeller + * @since 2.5.2 + * @see #invokeHandlerMethod + */ +public class HandlerMethodInvoker { + + /** + * We'll create a lot of these objects, so we don't want a new logger every time. + */ + private static final Log logger = LogFactory.getLog(HandlerMethodInvoker.class); + + private final HandlerMethodResolver methodResolver; + + private final WebBindingInitializer bindingInitializer; + + private final SessionAttributeStore sessionAttributeStore; + + private final ParameterNameDiscoverer parameterNameDiscoverer; + + private final WebArgumentResolver[] customArgumentResolvers; + + private final SimpleSessionStatus sessionStatus = new SimpleSessionStatus(); + + + public HandlerMethodInvoker(HandlerMethodResolver methodResolver) { + this(methodResolver, null); + } + + public HandlerMethodInvoker(HandlerMethodResolver methodResolver, WebBindingInitializer bindingInitializer) { + this(methodResolver, bindingInitializer, new DefaultSessionAttributeStore(), null); + } + + public HandlerMethodInvoker( + HandlerMethodResolver methodResolver, WebBindingInitializer bindingInitializer, + SessionAttributeStore sessionAttributeStore, ParameterNameDiscoverer parameterNameDiscoverer, + WebArgumentResolver... customArgumentResolvers) { + + this.methodResolver = methodResolver; + this.bindingInitializer = bindingInitializer; + this.sessionAttributeStore = sessionAttributeStore; + this.parameterNameDiscoverer = parameterNameDiscoverer; + this.customArgumentResolvers = customArgumentResolvers; + } + + + public final Object invokeHandlerMethod( + Method handlerMethod, Object handler, NativeWebRequest webRequest, ExtendedModelMap implicitModel) + throws Exception { + + Method handlerMethodToInvoke = BridgeMethodResolver.findBridgedMethod(handlerMethod); + try { + boolean debug = logger.isDebugEnabled(); + for (Method attributeMethod : this.methodResolver.getModelAttributeMethods()) { + Method attributeMethodToInvoke = BridgeMethodResolver.findBridgedMethod(attributeMethod); + Object[] args = resolveHandlerArguments(attributeMethodToInvoke, handler, webRequest, implicitModel); + if (debug) { + logger.debug("Invoking model attribute method: " + attributeMethodToInvoke); + } + Object attrValue = doInvokeMethod(attributeMethodToInvoke, handler, args); + String attrName = AnnotationUtils.findAnnotation(attributeMethodToInvoke, ModelAttribute.class).value(); + if ("".equals(attrName)) { + Class resolvedType = GenericTypeResolver.resolveReturnType(attributeMethodToInvoke, handler.getClass()); + attrName = Conventions.getVariableNameForReturnType(attributeMethodToInvoke, resolvedType, attrValue); + } + implicitModel.addAttribute(attrName, attrValue); + } + Object[] args = resolveHandlerArguments(handlerMethodToInvoke, handler, webRequest, implicitModel); + if (debug) { + logger.debug("Invoking request handler method: " + handlerMethodToInvoke); + } + return doInvokeMethod(handlerMethodToInvoke, handler, args); + } + catch (IllegalStateException ex) { + // Throw exception with full handler method context... + throw new HandlerMethodInvocationException(handlerMethodToInvoke, ex); + } + } + + @SuppressWarnings("unchecked") + private Object[] resolveHandlerArguments( + Method handlerMethod, Object handler, NativeWebRequest webRequest, ExtendedModelMap implicitModel) + throws Exception { + + Class[] paramTypes = handlerMethod.getParameterTypes(); + Object[] args = new Object[paramTypes.length]; + + for (int i = 0; i < args.length; i++) { + MethodParameter methodParam = new MethodParameter(handlerMethod, i); + methodParam.initParameterNameDiscovery(this.parameterNameDiscoverer); + GenericTypeResolver.resolveParameterType(methodParam, handler.getClass()); + String paramName = null; + boolean paramRequired = false; + String attrName = null; + Object[] paramAnns = methodParam.getParameterAnnotations(); + + for (int j = 0; j < paramAnns.length; j++) { + Object paramAnn = paramAnns[j]; + if (RequestParam.class.isInstance(paramAnn)) { + RequestParam requestParam = (RequestParam) paramAnn; + paramName = requestParam.value(); + paramRequired = requestParam.required(); + break; + } + else if (ModelAttribute.class.isInstance(paramAnn)) { + ModelAttribute attr = (ModelAttribute) paramAnn; + attrName = attr.value(); + } + } + if (paramName != null && attrName != null) { + throw new IllegalStateException("@RequestParam and @ModelAttribute are an exclusive choice -" + + "do not specify both on the same parameter: " + handlerMethod); + } + + Class paramType = methodParam.getParameterType(); + + if (paramName == null && attrName == null) { + Object argValue = resolveCommonArgument(methodParam, webRequest); + if (argValue != WebArgumentResolver.UNRESOLVED) { + args[i] = argValue; + } + else { + if (Model.class.isAssignableFrom(paramType) || Map.class.isAssignableFrom(paramType)) { + args[i] = implicitModel; + } + else if (SessionStatus.class.isAssignableFrom(paramType)) { + args[i] = this.sessionStatus; + } + else if (Errors.class.isAssignableFrom(paramType)) { + throw new IllegalStateException("Errors/BindingResult argument declared " + + "without preceding model attribute. Check your handler method signature!"); + } + else if (BeanUtils.isSimpleProperty(paramType)) { + paramName = ""; + } + else { + attrName = ""; + } + } + } + + if (paramName != null) { + args[i] = resolveRequestParam(paramName, paramRequired, methodParam, webRequest, handler); + } + else if (attrName != null) { + WebDataBinder binder = resolveModelAttribute(attrName, methodParam, implicitModel, webRequest, handler); + boolean assignBindingResult = (args.length > i + 1 && Errors.class.isAssignableFrom(paramTypes[i + 1])); + if (binder.getTarget() != null) { + doBind(webRequest, binder, !assignBindingResult); + } + args[i] = binder.getTarget(); + if (assignBindingResult) { + args[i + 1] = binder.getBindingResult(); + i++; + } + implicitModel.putAll(binder.getBindingResult().getModel()); + } + } + + return args; + } + + private void initBinder(Object handler, String attrName, WebDataBinder binder, NativeWebRequest webRequest) + throws Exception { + + if (this.bindingInitializer != null) { + this.bindingInitializer.initBinder(binder, webRequest); + } + if (handler != null) { + Set initBinderMethods = this.methodResolver.getInitBinderMethods(); + if (!initBinderMethods.isEmpty()) { + boolean debug = logger.isDebugEnabled(); + for (Method initBinderMethod : initBinderMethods) { + Method methodToInvoke = BridgeMethodResolver.findBridgedMethod(initBinderMethod); + String[] targetNames = AnnotationUtils.findAnnotation(methodToInvoke, InitBinder.class).value(); + if (targetNames.length == 0 || Arrays.asList(targetNames).contains(attrName)) { + Object[] initBinderArgs = resolveInitBinderArguments(handler, methodToInvoke, binder, webRequest); + if (debug) { + logger.debug("Invoking init-binder method: " + methodToInvoke); + } + Object returnValue = doInvokeMethod(methodToInvoke, handler, initBinderArgs); + if (returnValue != null) { + throw new IllegalStateException( + "InitBinder methods must not have a return value: " + methodToInvoke); + } + } + } + } + } + } + + private Object[] resolveInitBinderArguments(Object handler, Method initBinderMethod, + WebDataBinder binder, NativeWebRequest webRequest) throws Exception { + + Class[] initBinderParams = initBinderMethod.getParameterTypes(); + Object[] initBinderArgs = new Object[initBinderParams.length]; + + for (int i = 0; i < initBinderArgs.length; i++) { + MethodParameter methodParam = new MethodParameter(initBinderMethod, i); + methodParam.initParameterNameDiscovery(this.parameterNameDiscoverer); + GenericTypeResolver.resolveParameterType(methodParam, handler.getClass()); + String paramName = null; + boolean paramRequired = false; + Object[] paramAnns = methodParam.getParameterAnnotations(); + + for (int j = 0; j < paramAnns.length; j++) { + Object paramAnn = paramAnns[j]; + if (RequestParam.class.isInstance(paramAnn)) { + RequestParam requestParam = (RequestParam) paramAnn; + paramName = requestParam.value(); + paramRequired = requestParam.required(); + break; + } + else if (ModelAttribute.class.isInstance(paramAnn)) { + throw new IllegalStateException( + "@ModelAttribute is not supported on @InitBinder methods: " + initBinderMethod); + } + } + + if (paramName == null) { + Object argValue = resolveCommonArgument(methodParam, webRequest); + if (argValue != WebArgumentResolver.UNRESOLVED) { + initBinderArgs[i] = argValue; + } + else { + Class paramType = initBinderParams[i]; + if (paramType.isInstance(binder)) { + initBinderArgs[i] = binder; + } + else if (BeanUtils.isSimpleProperty(paramType)) { + paramName = ""; + } + else { + throw new IllegalStateException("Unsupported argument [" + paramType.getName() + + "] for @InitBinder method: " + initBinderMethod); + } + } + } + + if (paramName != null) { + initBinderArgs[i] = resolveRequestParam(paramName, paramRequired, methodParam, webRequest, null); + } + } + + return initBinderArgs; + } + + private Object resolveRequestParam(String paramName, boolean paramRequired, + MethodParameter methodParam, NativeWebRequest webRequest, Object handlerForInitBinderCall) + throws Exception { + + Class paramType = methodParam.getParameterType(); + if ("".equals(paramName)) { + paramName = methodParam.getParameterName(); + if (paramName == null) { + throw new IllegalStateException("No parameter specified for @RequestParam argument of type [" + + paramType.getName() + "], and no parameter name information found in class file either."); + } + } + Object paramValue = null; + if (webRequest.getNativeRequest() instanceof MultipartRequest) { + paramValue = ((MultipartRequest) webRequest.getNativeRequest()).getFile(paramName); + } + if (paramValue == null) { + String[] paramValues = webRequest.getParameterValues(paramName); + if (paramValues != null) { + paramValue = (paramValues.length == 1 ? paramValues[0] : paramValues); + } + } + if (paramValue == null) { + if (paramRequired) { + raiseMissingParameterException(paramName, paramType); + } + if (paramType.isPrimitive()) { + throw new IllegalStateException("Optional " + paramType + " parameter '" + paramName + + "' is not present but cannot be translated into a null value due to being declared as a " + + "primitive type. Consider declaring it as object wrapper for the corresponding primitive type."); + } + } + WebDataBinder binder = createBinder(webRequest, null, paramName); + initBinder(handlerForInitBinderCall, paramName, binder, webRequest); + return binder.convertIfNecessary(paramValue, paramType, methodParam); + } + + private WebDataBinder resolveModelAttribute(String attrName, MethodParameter methodParam, + ExtendedModelMap implicitModel, NativeWebRequest webRequest, Object handler) throws Exception { + + // Bind request parameter onto object... + String name = attrName; + if ("".equals(name)) { + name = Conventions.getVariableNameForParameter(methodParam); + } + Class paramType = methodParam.getParameterType(); + Object bindObject = null; + if (implicitModel.containsKey(name)) { + bindObject = implicitModel.get(name); + } + else if (this.methodResolver.isSessionAttribute(name, paramType)) { + bindObject = this.sessionAttributeStore.retrieveAttribute(webRequest, name); + if (bindObject == null) { + raiseSessionRequiredException("Session attribute '" + name + "' required - not found in session"); + } + } + else { + bindObject = BeanUtils.instantiateClass(paramType); + } + WebDataBinder binder = createBinder(webRequest, bindObject, name); + initBinder(handler, name, binder, webRequest); + return binder; + } + + @SuppressWarnings("unchecked") + public final void updateModelAttributes( + Object handler, Map mavModel, ExtendedModelMap implicitModel, NativeWebRequest webRequest) + throws Exception { + + if (this.methodResolver.hasSessionAttributes() && this.sessionStatus.isComplete()) { + for (String attrName : this.methodResolver.getActualSessionAttributeNames()) { + this.sessionAttributeStore.cleanupAttribute(webRequest, attrName); + } + } + + // Expose model attributes as session attributes, if required. + // Expose BindingResults for all attributes, making custom editors available. + Map model = (mavModel != null ? mavModel : implicitModel); + for (String attrName : new HashSet(model.keySet())) { + Object attrValue = model.get(attrName); + boolean isSessionAttr = + this.methodResolver.isSessionAttribute(attrName, (attrValue != null ? attrValue.getClass() : null)); + if (isSessionAttr && !this.sessionStatus.isComplete()) { + this.sessionAttributeStore.storeAttribute(webRequest, attrName, attrValue); + } + if (!attrName.startsWith(BindingResult.MODEL_KEY_PREFIX) && + (isSessionAttr || isBindingCandidate(attrValue))) { + String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + attrName; + if (mavModel != null && !model.containsKey(bindingResultKey)) { + WebDataBinder binder = createBinder(webRequest, attrValue, attrName); + initBinder(handler, attrName, binder, webRequest); + mavModel.put(bindingResultKey, binder.getBindingResult()); + } + } + } + } + + /** + * Determine whether the given value qualifies as a "binding candidate", + * i.e. might potentially be subject to bean-style data binding later on. + */ + protected boolean isBindingCandidate(Object value) { + return (value != null && !value.getClass().isArray() && !(value instanceof Collection) && + !(value instanceof Map) && !BeanUtils.isSimpleValueType(value.getClass())); + } + + private Object doInvokeMethod(Method method, Object target, Object[] args) throws Exception { + ReflectionUtils.makeAccessible(method); + try { + return method.invoke(target, args); + } + catch (InvocationTargetException ex) { + ReflectionUtils.rethrowException(ex.getTargetException()); + } + throw new IllegalStateException("Should never get here"); + } + + + protected void raiseMissingParameterException(String paramName, Class paramType) throws Exception { + throw new IllegalStateException("Missing parameter '" + paramName + "' of type [" + paramType.getName() + "]"); + } + + protected void raiseSessionRequiredException(String message) throws Exception { + throw new IllegalStateException(message); + } + + protected WebDataBinder createBinder(NativeWebRequest webRequest, Object target, String objectName) + throws Exception { + + return new WebRequestDataBinder(target, objectName); + } + + protected void doBind(NativeWebRequest webRequest, WebDataBinder binder, boolean failOnErrors) + throws Exception { + + WebRequestDataBinder requestBinder = (WebRequestDataBinder) binder; + requestBinder.bind(webRequest); + if (failOnErrors) { + requestBinder.closeNoCatch(); + } + } + + protected Object resolveCommonArgument(MethodParameter methodParameter, NativeWebRequest webRequest) + throws Exception { + + // Invoke custom argument resolvers if present... + if (this.customArgumentResolvers != null) { + for (WebArgumentResolver argumentResolver : this.customArgumentResolvers) { + Object value = argumentResolver.resolveArgument(methodParameter, webRequest); + if (value != WebArgumentResolver.UNRESOLVED) { + return value; + } + } + } + + // Resolution of standard parameter types... + Class paramType = methodParameter.getParameterType(); + Object value = resolveStandardArgument(paramType, webRequest); + if (value != WebArgumentResolver.UNRESOLVED && !ClassUtils.isAssignableValue(paramType, value)) { + throw new IllegalStateException("Standard argument type [" + paramType.getName() + + "] resolved to incompatible value of type [" + (value != null ? value.getClass() : null) + + "]. Consider declaring the argument type in a less specific fashion."); + } + return value; + } + + protected Object resolveStandardArgument(Class parameterType, NativeWebRequest webRequest) + throws Exception { + + if (WebRequest.class.isAssignableFrom(parameterType)) { + return webRequest; + } + return WebArgumentResolver.UNRESOLVED; + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodResolver.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodResolver.java new file mode 100644 index 00000000000..468e2f39fb7 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodResolver.java @@ -0,0 +1,137 @@ +/* + * Copyright 2002-2008 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.web.bind.annotation.support; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.web.bind.annotation.InitBinder; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.SessionAttributes; + +/** + * Support class for resolving web method annotations in a handler type. + * Processes @RequestMapping, @InitBinder, + * @ModelAttribute and @SessionAttributes. + * + *

Used by {@link org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter} + * and {@link org.springframework.web.portlet.mvc.annotation.AnnotationMethodHandlerAdapter}. + * + * @author Juergen Hoeller + * @since 2.5.2 + * @see org.springframework.web.bind.annotation.RequestMapping + * @see org.springframework.web.bind.annotation.InitBinder + * @see org.springframework.web.bind.annotation.ModelAttribute + * @see org.springframework.web.bind.annotation.SessionAttributes + */ +public class HandlerMethodResolver { + + private final Set handlerMethods = new LinkedHashSet(); + + private final Set initBinderMethods = new LinkedHashSet(); + + private final Set modelAttributeMethods = new LinkedHashSet(); + + private final RequestMapping typeLevelMapping; + + private final boolean sessionAttributesFound; + + private final Set sessionAttributeNames = new HashSet(); + + private final Set sessionAttributeTypes = new HashSet(); + + private final Set actualSessionAttributeNames = Collections.synchronizedSet(new HashSet(4)); + + + /** + * Create a new HandlerMethodResolver for the specified handler type. + * @param handlerType the handler class to introspect + */ + public HandlerMethodResolver(final Class handlerType) { + ReflectionUtils.doWithMethods(handlerType, new ReflectionUtils.MethodCallback() { + public void doWith(Method method) { + if (method.isAnnotationPresent(RequestMapping.class)) { + handlerMethods.add(ClassUtils.getMostSpecificMethod(method, handlerType)); + } + else if (method.isAnnotationPresent(InitBinder.class)) { + initBinderMethods.add(ClassUtils.getMostSpecificMethod(method, handlerType)); + } + else if (method.isAnnotationPresent(ModelAttribute.class)) { + modelAttributeMethods.add(ClassUtils.getMostSpecificMethod(method, handlerType)); + } + } + }); + this.typeLevelMapping = handlerType.getAnnotation(RequestMapping.class); + SessionAttributes sessionAttributes = handlerType.getAnnotation(SessionAttributes.class); + this.sessionAttributesFound = (sessionAttributes != null); + if (this.sessionAttributesFound) { + this.sessionAttributeNames.addAll(Arrays.asList(sessionAttributes.value())); + this.sessionAttributeTypes.addAll(Arrays.asList(sessionAttributes.types())); + } + } + + + public final boolean hasHandlerMethods() { + return !this.handlerMethods.isEmpty(); + } + + public final Set getHandlerMethods() { + return this.handlerMethods; + } + + public final Set getInitBinderMethods() { + return this.initBinderMethods; + } + + public final Set getModelAttributeMethods() { + return this.modelAttributeMethods; + } + + public boolean hasTypeLevelMapping() { + return (this.typeLevelMapping != null); + } + + public RequestMapping getTypeLevelMapping() { + return this.typeLevelMapping; + } + + public boolean hasSessionAttributes() { + return this.sessionAttributesFound; + } + + public boolean isSessionAttribute(String attrName, Class attrType) { + if (this.sessionAttributeNames.contains(attrName) || this.sessionAttributeTypes.contains(attrType)) { + this.actualSessionAttributeNames.add(attrName); + return true; + } + else { + return false; + } + } + + public Set getActualSessionAttributeNames() { + return this.actualSessionAttributeNames; + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/support/package.html b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/support/package.html new file mode 100644 index 00000000000..5d2b9502b81 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/support/package.html @@ -0,0 +1,7 @@ + + + +Support classes for web annotation processing. + + + diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/package.html b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/package.html new file mode 100644 index 00000000000..da10d9cdede --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/package.html @@ -0,0 +1,7 @@ + + + +Provides web-specific data binding functionality. + + + diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/support/ConfigurableWebBindingInitializer.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/support/ConfigurableWebBindingInitializer.java new file mode 100644 index 00000000000..27a1361b0d2 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/support/ConfigurableWebBindingInitializer.java @@ -0,0 +1,135 @@ +/* + * Copyright 2002-2007 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.web.bind.support; + +import org.springframework.beans.PropertyEditorRegistrar; +import org.springframework.validation.BindingErrorProcessor; +import org.springframework.validation.MessageCodesResolver; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.context.request.WebRequest; + +/** + * Convenient {@link WebBindingInitializer} for declarative configuration + * in a Spring application context. Allows for reusing pre-configured + * initializers with multiple controller/handlers. + * + * @author Juergen Hoeller + * @since 2.5 + * @see #setDirectFieldAccess + * @see #setMessageCodesResolver + * @see #setBindingErrorProcessor + * @see #setPropertyEditorRegistrar + */ +public class ConfigurableWebBindingInitializer implements WebBindingInitializer { + + private boolean directFieldAccess = false; + + private MessageCodesResolver messageCodesResolver; + + private BindingErrorProcessor bindingErrorProcessor; + + private PropertyEditorRegistrar[] propertyEditorRegistrars; + + + /** + * Set whether to use direct field access instead of bean property access. + *

Default is false, using bean property access. + * Switch this to true for enforcing direct field access. + */ + public final void setDirectFieldAccess(boolean directFieldAccess) { + this.directFieldAccess = directFieldAccess; + } + + /** + * Set the strategy to use for resolving errors into message codes. + * Applies the given strategy to all data binders used by this controller. + *

Default is null, i.e. using the default strategy of + * the data binder. + * @see org.springframework.validation.DataBinder#setMessageCodesResolver + */ + public final void setMessageCodesResolver(MessageCodesResolver messageCodesResolver) { + this.messageCodesResolver = messageCodesResolver; + } + + /** + * Return the strategy to use for resolving errors into message codes. + */ + public final MessageCodesResolver getMessageCodesResolver() { + return this.messageCodesResolver; + } + + /** + * Set the strategy to use for processing binding errors, that is, + * required field errors and PropertyAccessExceptions. + *

Default is null, that is, using the default strategy + * of the data binder. + * @see org.springframework.validation.DataBinder#setBindingErrorProcessor + */ + public final void setBindingErrorProcessor(BindingErrorProcessor bindingErrorProcessor) { + this.bindingErrorProcessor = bindingErrorProcessor; + } + + /** + * Return the strategy to use for processing binding errors. + */ + public final BindingErrorProcessor getBindingErrorProcessor() { + return this.bindingErrorProcessor; + } + + /** + * Specify a single PropertyEditorRegistrar to be applied + * to every DataBinder that this controller uses. + */ + public final void setPropertyEditorRegistrar(PropertyEditorRegistrar propertyEditorRegistrar) { + this.propertyEditorRegistrars = new PropertyEditorRegistrar[] {propertyEditorRegistrar}; + } + + /** + * Specify multiple PropertyEditorRegistrars to be applied + * to every DataBinder that this controller uses. + */ + public final void setPropertyEditorRegistrars(PropertyEditorRegistrar[] propertyEditorRegistrars) { + this.propertyEditorRegistrars = propertyEditorRegistrars; + } + + /** + * Return the PropertyEditorRegistrars to be applied + * to every DataBinder that this controller uses. + */ + public final PropertyEditorRegistrar[] getPropertyEditorRegistrars() { + return this.propertyEditorRegistrars; + } + + + public void initBinder(WebDataBinder binder, WebRequest request) { + if (this.directFieldAccess) { + binder.initDirectFieldAccess(); + } + if (this.messageCodesResolver != null) { + binder.setMessageCodesResolver(this.messageCodesResolver); + } + if (this.bindingErrorProcessor != null) { + binder.setBindingErrorProcessor(this.bindingErrorProcessor); + } + if (this.propertyEditorRegistrars != null) { + for (int i = 0; i < this.propertyEditorRegistrars.length; i++) { + this.propertyEditorRegistrars[i].registerCustomEditors(binder); + } + } + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/support/DefaultSessionAttributeStore.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/support/DefaultSessionAttributeStore.java new file mode 100644 index 00000000000..6f0e512a1a9 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/support/DefaultSessionAttributeStore.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2007 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.web.bind.support; + +import org.springframework.util.Assert; +import org.springframework.web.context.request.WebRequest; + +/** + * Default implementation of the {@link SessionAttributeStore} interface, + * storing the attributes in the WebRequest session (i.e. HttpSession + * or PortletSession). + * + * @author Juergen Hoeller + * @since 2.5 + * @see #setAttributeNamePrefix + * @see org.springframework.web.context.request.WebRequest#setAttribute + * @see org.springframework.web.context.request.WebRequest#getAttribute + * @see org.springframework.web.context.request.WebRequest#removeAttribute + */ +public class DefaultSessionAttributeStore implements SessionAttributeStore { + + private String attributeNamePrefix = ""; + + + /** + * Specify a prefix to use for the attribute names in the backend session. + *

Default is to use no prefix, storing the session attributes with the + * same name as in the model. + */ + public void setAttributeNamePrefix(String attributeNamePrefix) { + this.attributeNamePrefix = (attributeNamePrefix != null ? attributeNamePrefix : ""); + } + + + public void storeAttribute(WebRequest request, String attributeName, Object attributeValue) { + Assert.notNull(request, "WebRequest must not be null"); + Assert.notNull(attributeName, "Attribute name must not be null"); + Assert.notNull(attributeValue, "Attribute value must not be null"); + String storeAttributeName = getAttributeNameInSession(request, attributeName); + request.setAttribute(storeAttributeName, attributeValue, WebRequest.SCOPE_SESSION); + } + + public Object retrieveAttribute(WebRequest request, String attributeName) { + Assert.notNull(request, "WebRequest must not be null"); + Assert.notNull(attributeName, "Attribute name must not be null"); + String storeAttributeName = getAttributeNameInSession(request, attributeName); + return request.getAttribute(storeAttributeName, WebRequest.SCOPE_SESSION); + } + + public void cleanupAttribute(WebRequest request, String attributeName) { + Assert.notNull(request, "WebRequest must not be null"); + Assert.notNull(attributeName, "Attribute name must not be null"); + String storeAttributeName = getAttributeNameInSession(request, attributeName); + request.removeAttribute(storeAttributeName, WebRequest.SCOPE_SESSION); + } + + + /** + * Calculate the attribute name in the backend session. + *

The default implementation simply prepends the configured + * {@link #setAttributeNamePrefix "attributeNamePrefix"}, if any. + * @param request the current request + * @param attributeName the name of the attribute + * @return the attribute name in the backend session + */ + protected String getAttributeNameInSession(WebRequest request, String attributeName) { + return this.attributeNamePrefix + attributeName; + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/support/SessionAttributeStore.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/support/SessionAttributeStore.java new file mode 100644 index 00000000000..e428074dad5 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/support/SessionAttributeStore.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2007 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.web.bind.support; + +import org.springframework.web.context.request.WebRequest; + +/** + * Strategy interface for storing model attributes in a backend session. + * + * @author Juergen Hoeller + * @since 2.5 + * @see org.springframework.web.bind.annotation.SessionAttributes + */ +public interface SessionAttributeStore { + + /** + * Store the supplied attribute in the backend session. + *

Can be called for new attributes as well as for existing attributes. + * In the latter case, this signals that the attribute value may have been modified. + * @param request the current request + * @param attributeName the name of the attribute + * @param attributeValue the attribute value to store + */ + void storeAttribute(WebRequest request, String attributeName, Object attributeValue); + + /** + * Retrieve the specified attribute from the backend session. + *

This will typically be called with the expectation that the + * attribute is already present, with an exception to be thrown + * if this method returns null. + * @param request the current request + * @param attributeName the name of the attribute + * @return the current attribute value, or null if none + */ + Object retrieveAttribute(WebRequest request, String attributeName); + + /** + * Clean up the specified attribute in the backend session. + *

Indicates that the attribute name will not be used anymore. + * @param request the current request + * @param attributeName the name of the attribute + */ + void cleanupAttribute(WebRequest request, String attributeName); + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/support/SessionStatus.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/support/SessionStatus.java new file mode 100644 index 00000000000..cf48169e9f1 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/support/SessionStatus.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2007 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.web.bind.support; + +/** + * Simple interface that can be injected into handler methods, allowing them to + * signal that their session processing is complete. The handler invoker may + * then follow up with appropriate cleanup, e.g. of session attributes which + * have been implicitly created during this handler's processing (according to + * the + * {@link org.springframework.web.bind.annotation.SessionAttributes @SessionAttributes} + * annotation). + * + * @author Juergen Hoeller + * @since 2.5 + * @see org.springframework.web.bind.annotation.RequestMapping + * @see org.springframework.web.bind.annotation.SessionAttributes + */ +public interface SessionStatus { + + /** + * Mark the current handler's session processing as complete, allowing for + * cleanup of session attributes. + */ + void setComplete(); + + /** + * Return whether the current handler's session processing has been marked + * as complete. + */ + boolean isComplete(); + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/support/SimpleSessionStatus.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/support/SimpleSessionStatus.java new file mode 100644 index 00000000000..1839316574c --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/support/SimpleSessionStatus.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2007 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.web.bind.support; + +/** + * Simple implementation of the {@link SessionStatus} interface, + * keeping the complete flag as an instance variable. + * + * @author Juergen Hoeller + * @since 2.5 + */ +public class SimpleSessionStatus implements SessionStatus { + + private boolean complete = false; + + + public void setComplete() { + this.complete = true; + } + + public boolean isComplete() { + return this.complete; + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/support/WebArgumentResolver.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/support/WebArgumentResolver.java new file mode 100644 index 00000000000..325806b8879 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/support/WebArgumentResolver.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2008 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.web.bind.support; + +import org.springframework.core.MethodParameter; +import org.springframework.web.context.request.NativeWebRequest; + +/** + * SPI for resolving custom arguments for a specific handler method parameter. + * Typically implemented to detect sppecial parameter types, resolving + * well-known argument values for them. + * + *

A typical implementation could look like as follows: + * + *

+ * public class MySpecialArgumentResolver implements ArgumentResolver {
+ *
+ *   public Object resolveArgument(MethodParameter methodParameter, NativeWebRequest webRequest) {
+ *     if (methodParameter.getParameterType().equals(MySpecialArg.class)) {
+ *       return new MySpecialArg("myValue");
+ *     }
+ *     return UNRESOLVED;
+ *   }
+ * }
+ * + * @author Juergen Hoeller + * @since 2.5.2 + * @see org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter#setCustomArgumentResolvers + * @see org.springframework.web.portlet.mvc.annotation.AnnotationMethodHandlerAdapter#setCustomArgumentResolvers + */ +public interface WebArgumentResolver { + + /** + * Marker to be returned when the resolver does not know how to + * handle the given method parameter. + */ + Object UNRESOLVED = new Object(); + + + /** + * Resolve an argument for the given handler method parameter within the given web request. + * @param methodParameter the handler method parameter to resolve + * @param webRequest the current web request, allowing access to the native request as well + * @return the argument value, or UNRESOLVED if not resolvable + * @throws Exception in case of resolution failure + */ + Object resolveArgument(MethodParameter methodParameter, NativeWebRequest webRequest) throws Exception; + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/support/WebBindingInitializer.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/support/WebBindingInitializer.java new file mode 100644 index 00000000000..be7e0479dee --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/support/WebBindingInitializer.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2007 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.web.bind.support; + +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.context.request.WebRequest; + +/** + * Callback interface for initializing a {@link org.springframework.web.bind.WebDataBinder} + * for performing data binding in the context of a specific web request. + * + * @author Juergen Hoeller + * @since 2.5 + */ +public interface WebBindingInitializer { + + /** + * Initialize the given DataBinder for the given request. + * @param binder the DataBinder to initialize + * @param request the web request that the data binding happens within + */ + void initBinder(WebDataBinder binder, WebRequest request); + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java new file mode 100644 index 00000000000..5f136938174 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java @@ -0,0 +1,125 @@ +/* + * Copyright 2002-2008 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.web.bind.support; + +import org.springframework.beans.MutablePropertyValues; +import org.springframework.validation.BindException; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.multipart.MultipartRequest; + +/** + * Special {@link org.springframework.validation.DataBinder} to perform data binding + * from web request parameters to JavaBeans, including support for multipart files. + * + *

See the DataBinder/WebDataBinder superclasses for customization options, + * which include specifying allowed/required fields, and registering custom + * property editors. + * + *

Can also used for manual data binding in custom web controllers or interceptors + * that build on Spring's {@link org.springframework.web.context.request.WebRequest} + * abstraction: e.g. in a {@link org.springframework.web.context.request.WebRequestInterceptor} + * implementation. Simply instantiate a WebRequestDataBinder for each binding + * process, and invoke bind with the current WebRequest as argument: + * + *

+ * MyBean myBean = new MyBean();
+ * // apply binder to custom target object
+ * WebRequestDataBinder binder = new WebRequestDataBinder(myBean);
+ * // register custom editors, if desired
+ * binder.registerCustomEditor(...);
+ * // trigger actual binding of request parameters
+ * binder.bind(request);
+ * // optionally evaluate binding errors
+ * Errors errors = binder.getErrors();
+ * ...
+ * + * @author Juergen Hoeller + * @since 2.5.2 + * @see #bind(org.springframework.web.context.request.WebRequest) + * @see #registerCustomEditor + * @see #setAllowedFields + * @see #setRequiredFields + * @see #setFieldMarkerPrefix + */ +public class WebRequestDataBinder extends WebDataBinder { + + /** + * Create a new WebRequestDataBinder instance, with default object name. + * @param target the target object to bind onto (or null + * if the binder is just used to convert a plain parameter value) + * @see #DEFAULT_OBJECT_NAME + */ + public WebRequestDataBinder(Object target) { + super(target); + } + + /** + * Create a new WebRequestDataBinder instance. + * @param target the target object to bind onto (or null + * if the binder is just used to convert a plain parameter value) + * @param objectName the name of the target object + */ + public WebRequestDataBinder(Object target, String objectName) { + super(target, objectName); + } + + + /** + * Bind the parameters of the given request to this binder's target, + * also binding multipart files in case of a multipart request. + *

This call can create field errors, representing basic binding + * errors like a required field (code "required"), or type mismatch + * between value and bean property (code "typeMismatch"). + *

Multipart files are bound via their parameter name, just like normal + * HTTP parameters: i.e. "uploadedFile" to an "uploadedFile" bean property, + * invoking a "setUploadedFile" setter method. + *

The type of the target property for a multipart file can be MultipartFile, + * byte[], or String. The latter two receive the contents of the uploaded file; + * all metadata like original file name, content type, etc are lost in those cases. + * @param request request with parameters to bind (can be multipart) + * @see org.springframework.web.multipart.MultipartRequest + * @see org.springframework.web.multipart.MultipartFile + * @see #bindMultipartFiles + * @see #bind(org.springframework.beans.PropertyValues) + */ + public void bind(WebRequest request) { + MutablePropertyValues mpvs = new MutablePropertyValues(request.getParameterMap()); + if (request instanceof NativeWebRequest) { + Object nativeRequest = ((NativeWebRequest) request).getNativeRequest(); + if (nativeRequest instanceof MultipartRequest) { + MultipartRequest multipartRequest = (MultipartRequest) request; + bindMultipartFiles(multipartRequest.getFileMap(), mpvs); + } + } + doBind(mpvs); + } + + /** + * Treats errors as fatal. + *

Use this method only if it's an error if the input isn't valid. + * This might be appropriate if all input is from dropdowns, for example. + * @throws BindException if binding errors have been encountered + */ + public void closeNoCatch() throws BindException { + if (getBindingResult().hasErrors()) { + throw new BindException(getBindingResult()); + } + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/support/package.html b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/support/package.html new file mode 100644 index 00000000000..eba7e423cae --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/support/package.html @@ -0,0 +1,7 @@ + + + +Support classes for web data binding. + + + diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/MaxUploadSizeExceededException.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/MaxUploadSizeExceededException.java new file mode 100644 index 00000000000..fe9892dcfc5 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/MaxUploadSizeExceededException.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2005 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.web.multipart; + +/** + * MultipartException subclass thrown when an upload exceeds the + * maximum upload size allowed. + * + * @author Juergen Hoeller + * @since 1.0.1 + */ +public class MaxUploadSizeExceededException extends MultipartException { + + private final long maxUploadSize; + + + /** + * Constructor for MaxUploadSizeExceededException. + * @param maxUploadSize the maximum upload size allowed + */ + public MaxUploadSizeExceededException(long maxUploadSize) { + this(maxUploadSize, null); + } + + /** + * Constructor for MaxUploadSizeExceededException. + * @param maxUploadSize the maximum upload size allowed + * @param ex root cause from multipart parsing API in use + */ + public MaxUploadSizeExceededException(long maxUploadSize, Throwable ex) { + super("Maximum upload size of " + maxUploadSize + " bytes exceeded", ex); + this.maxUploadSize = maxUploadSize; + } + + + /** + * Return the maximum upload size allowed. + */ + public long getMaxUploadSize() { + return maxUploadSize; + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/MultipartException.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/MultipartException.java new file mode 100644 index 00000000000..64360f4d8d9 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/MultipartException.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2007 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.web.multipart; + +import org.springframework.core.NestedRuntimeException; + +/** + * Exception thrown when multipart resolution fails. + * + * @author Trevor D. Cook + * @author Juergen Hoeller + * @since 29.09.2003 + * @see MultipartResolver#resolveMultipart + * @see org.springframework.web.multipart.support.MultipartFilter + */ +public class MultipartException extends NestedRuntimeException { + + /** + * Constructor for MultipartException. + * @param msg the detail message + */ + public MultipartException(String msg) { + super(msg); + } + + /** + * Constructor for MultipartException. + * @param msg the detail message + * @param cause the root cause from the multipart parsing API in use + */ + public MultipartException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/MultipartFile.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/MultipartFile.java new file mode 100644 index 00000000000..b45a5a411e1 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/MultipartFile.java @@ -0,0 +1,103 @@ +/* + * Copyright 2002-2006 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.web.multipart; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +/** + * A representation of an uploaded file received in a multipart request. + * + *

The file contents are either stored in memory or temporarily on disk. + * In either case, the user is responsible for copying file contents to a + * session-level or persistent store as and if desired. The temporary storages + * will be cleared at the end of request processing. + * + * @author Juergen Hoeller + * @author Trevor D. Cook + * @since 29.09.2003 + * @see org.springframework.web.multipart.MultipartHttpServletRequest + * @see org.springframework.web.multipart.MultipartResolver + */ +public interface MultipartFile { + + /** + * Return the name of the parameter in the multipart form. + * @return the name of the parameter (never null or empty) + */ + String getName(); + + /** + * Return the original filename in the client's filesystem. + *

This may contain path information depending on the browser used, + * but it typically will not with any other than Opera. + * @return the original filename, or the empty String if no file + * has been chosen in the multipart form + */ + String getOriginalFilename(); + + /** + * Return the content type of the file. + * @return the content type, or null if not defined + * (or no file has been chosen in the multipart form) + */ + String getContentType(); + + /** + * Return whether the uploaded file is empty, that is, either no file has + * been chosen in the multipart form or the chosen file has no content. + */ + boolean isEmpty(); + + /** + * Return the size of the file in bytes. + * @return the size of the file, or 0 if empty + */ + long getSize(); + + /** + * Return the contents of the file as an array of bytes. + * @return the contents of the file as bytes, or an empty byte array if empty + * @throws IOException in case of access errors (if the temporary store fails) + */ + byte[] getBytes() throws IOException; + + /** + * Return an InputStream to read the contents of the file from. + * The user is responsible for closing the stream. + * @return the contents of the file as stream, or an empty stream if empty + * @throws IOException in case of access errors (if the temporary store fails) + */ + InputStream getInputStream() throws IOException; + + /** + * Transfer the received file to the given destination file. + *

This may either move the file in the filesystem, copy the file in the + * filesystem, or save memory-held contents to the destination file. + * If the destination file already exists, it will be deleted first. + *

If the file has been moved in the filesystem, this operation cannot + * be invoked again. Therefore, call this method just once to be able to + * work with any storage mechanism. + * @param dest the destination file + * @throws IOException in case of reading or writing errors + * @throws IllegalStateException if the file has already been moved + * in the filesystem and is not available anymore for another transfer + */ + void transferTo(File dest) throws IOException, IllegalStateException; + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/MultipartHttpServletRequest.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/MultipartHttpServletRequest.java new file mode 100644 index 00000000000..249e508b3eb --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/MultipartHttpServletRequest.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2008 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.web.multipart; + +import javax.servlet.http.HttpServletRequest; + +/** + * Provides additional methods for dealing with multipart content within a + * servlet request, allowing to access uploaded files. + * Implementations also need to override the standard + * {@link javax.servlet.ServletRequest} methods for parameter access, making + * multipart parameters available. + * + *

A concrete implementation is + * {@link org.springframework.web.multipart.support.DefaultMultipartHttpServletRequest}. + * As an intermediate step, + * {@link org.springframework.web.multipart.support.AbstractMultipartHttpServletRequest} + * can be subclassed. + * + * @author Juergen Hoeller + * @author Trevor D. Cook + * @since 29.09.2003 + * @see MultipartResolver + * @see MultipartFile + * @see javax.servlet.http.HttpServletRequest#getParameter + * @see javax.servlet.http.HttpServletRequest#getParameterNames + * @see javax.servlet.http.HttpServletRequest#getParameterMap + * @see org.springframework.web.multipart.support.DefaultMultipartHttpServletRequest + * @see org.springframework.web.multipart.support.AbstractMultipartHttpServletRequest + */ +public interface MultipartHttpServletRequest extends HttpServletRequest, MultipartRequest { + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/MultipartRequest.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/MultipartRequest.java new file mode 100644 index 00000000000..a7591051bc7 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/MultipartRequest.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2008 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.web.multipart; + +import java.util.Iterator; +import java.util.Map; + +/** + * This interface defines the multipart request access operations + * that are exposed for actual multipart requests. It is extended + * by {@link MultipartHttpServletRequest} and the Portlet + * {@link org.springframework.web.portlet.multipart.MultipartActionRequest}. + * + * @author Juergen Hoeller + * @since 2.5.2 + */ +public interface MultipartRequest { + + /** + * Return an {@link java.util.Iterator} of String objects containing the + * parameter names of the multipart files contained in this request. These + * are the field names of the form (like with normal parameters), not the + * original file names. + * @return the names of the files + */ + Iterator getFileNames(); + + /** + * Return the contents plus description of an uploaded file in this request, + * or null if it does not exist. + * @param name a String specifying the parameter name of the multipart file + * @return the uploaded content in the form of a {@link org.springframework.web.multipart.MultipartFile} object + */ + MultipartFile getFile(String name); + + /** + * Return a {@link java.util.Map} of the multipart files contained in this request. + * @return a map containing the parameter names as keys, and the + * {@link org.springframework.web.multipart.MultipartFile} objects as values + * @see MultipartFile + */ + Map getFileMap(); + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/MultipartResolver.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/MultipartResolver.java new file mode 100644 index 00000000000..0e6a5baaba1 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/MultipartResolver.java @@ -0,0 +1,127 @@ +/* + * Copyright 2002-2007 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.web.multipart; + +import javax.servlet.http.HttpServletRequest; + +/** + * A strategy interface for multipart file upload resolution in accordance + * with RFC 1867. + * Implementations are typically usable both within an application context + * and standalone. + * + *

There is only one concrete implementation included in Spring, + * as of Spring 2.5: + *

+ * + *

There is no default resolver implementation used for Spring + * {@link org.springframework.web.servlet.DispatcherServlet DispatcherServlets}, + * as an application might choose to parse its multipart requests itself. To define + * an implementation, create a bean with the id "multipartResolver" in a + * {@link org.springframework.web.servlet.DispatcherServlet DispatcherServlet's} + * application context. Such a resolver gets applied to all requests handled + * by that {@link org.springframework.web.servlet.DispatcherServlet}. + * + *

If a {@link org.springframework.web.servlet.DispatcherServlet} detects + * a multipart request, it will resolve it via the configured + * {@link org.springframework.web.multipart.MultipartResolver} and pass on a + * wrapped {@link javax.servlet.http.HttpServletRequest}. + * Controllers can then cast their given request to the + * {@link org.springframework.web.multipart.MultipartHttpServletRequest} + * interface, which permits access to any + * {@link org.springframework.web.multipart.MultipartFile MultipartFiles}. + * Note that this cast is only supported in case of an actual multipart request. + * + *

+ * public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) {
+ *   MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
+ *   MultipartFile multipartFile = multipartRequest.getFile("image");
+ *   ...
+ * }
+ * + * Instead of direct access, command or form controllers can register a + * {@link org.springframework.web.multipart.support.ByteArrayMultipartFileEditor} + * or {@link org.springframework.web.multipart.support.StringMultipartFileEditor} + * with their data binder, to automatically apply multipart content to command + * bean properties. + * + *

As an alternative to using a + * {@link org.springframework.web.multipart.MultipartResolver} with a + * {@link org.springframework.web.servlet.DispatcherServlet}, + * a {@link org.springframework.web.multipart.support.MultipartFilter} can be + * registered in web.xml. It will delegate to a corresponding + * {@link org.springframework.web.multipart.MultipartResolver} bean in the root + * application context. This is mainly intended for applications that do not + * use Spring's own web MVC framework. + * + *

Note: There is hardly ever a need to access the + * {@link org.springframework.web.multipart.MultipartResolver} itself + * from application code. It will simply do its work behind the scenes, + * making + * {@link org.springframework.web.multipart.MultipartHttpServletRequest MultipartHttpServletRequests} + * available to controllers. + * + * @author Juergen Hoeller + * @author Trevor D. Cook + * @since 29.09.2003 + * @see MultipartHttpServletRequest + * @see MultipartFile + * @see org.springframework.web.multipart.commons.CommonsMultipartResolver + * @see org.springframework.web.multipart.support.ByteArrayMultipartFileEditor + * @see org.springframework.web.multipart.support.StringMultipartFileEditor + * @see org.springframework.web.servlet.DispatcherServlet + */ +public interface MultipartResolver { + + /** + * Determine if the given request contains multipart content. + *

Will typically check for content type "multipart/form-data", but the actually + * accepted requests might depend on the capabilities of the resolver implementation. + * @param request the servlet request to be evaluated + * @return whether the request contains multipart content + */ + boolean isMultipart(HttpServletRequest request); + + /** + * Parse the given HTTP request into multipart files and parameters, + * and wrap the request inside a + * {@link org.springframework.web.multipart.MultipartHttpServletRequest} object + * that provides access to file descriptors and makes contained + * parameters accessible via the standard ServletRequest methods. + * @param request the servlet request to wrap (must be of a multipart content type) + * @return the wrapped servlet request + * @throws MultipartException if the servlet request is not multipart, or if + * implementation-specific problems are encountered (such as exceeding file size limits) + * @see MultipartHttpServletRequest#getFile + * @see MultipartHttpServletRequest#getFileNames + * @see MultipartHttpServletRequest#getFileMap + * @see javax.servlet.http.HttpServletRequest#getParameter + * @see javax.servlet.http.HttpServletRequest#getParameterNames + * @see javax.servlet.http.HttpServletRequest#getParameterMap + */ + MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException; + + /** + * Cleanup any resources used for the multipart handling, + * like a storage for the uploaded files. + * @param request the request to cleanup resources for + */ + void cleanupMultipart(MultipartHttpServletRequest request); + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/commons/CommonsFileUploadSupport.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/commons/CommonsFileUploadSupport.java new file mode 100644 index 00000000000..733047a5817 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/commons/CommonsFileUploadSupport.java @@ -0,0 +1,321 @@ +/* + * Copyright 2002-2007 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.web.multipart.commons; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.apache.commons.fileupload.FileItem; +import org.apache.commons.fileupload.FileItemFactory; +import org.apache.commons.fileupload.FileUpload; +import org.apache.commons.fileupload.disk.DiskFileItemFactory; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.io.Resource; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartException; +import org.springframework.web.util.WebUtils; + +/** + * Base class for multipart resolvers that use Jakarta Commons FileUpload + * 1.1 or higher. + * + *

Provides common configuration properties and parsing functionality + * for multipart requests, using a Map of Spring CommonsMultipartFile instances + * as representation of uploaded files and a String-based parameter Map as + * representation of uploaded form fields. + * + *

Subclasses implement concrete resolution strategies for Servlet or Portlet + * environments: see CommonsMultipartResolver and CommonsPortletMultipartResolver, + * respectively. This base class is not tied to either of those APIs, factoring + * out common functionality. + * + * @author Juergen Hoeller + * @since 2.0 + * @see CommonsMultipartFile + * @see CommonsMultipartResolver + * @see org.springframework.web.portlet.multipart.CommonsPortletMultipartResolver + */ +public abstract class CommonsFileUploadSupport { + + protected final Log logger = LogFactory.getLog(getClass()); + + private final DiskFileItemFactory fileItemFactory; + + private final FileUpload fileUpload; + + private boolean uploadTempDirSpecified = false; + + + /** + * Instantiate a new CommonsFileUploadSupport with its + * corresponding FileItemFactory and FileUpload instances. + * @see #newFileItemFactory + * @see #newFileUpload + */ + public CommonsFileUploadSupport() { + this.fileItemFactory = newFileItemFactory(); + this.fileUpload = newFileUpload(getFileItemFactory()); + } + + + /** + * Return the underlying org.apache.commons.fileupload.disk.DiskFileItemFactory + * instance. There is hardly any need to access this. + * @return the underlying DiskFileItemFactory instance + */ + public DiskFileItemFactory getFileItemFactory() { + return this.fileItemFactory; + } + + /** + * Return the underlying org.apache.commons.fileupload.FileUpload + * instance. There is hardly any need to access this. + * @return the underlying FileUpload instance + */ + public FileUpload getFileUpload() { + return this.fileUpload; + } + + /** + * Set the maximum allowed size (in bytes) before uploads are refused. + * -1 indicates no limit (the default). + * @param maxUploadSize the maximum upload size allowed + * @see org.apache.commons.fileupload.FileUploadBase#setSizeMax + */ + public void setMaxUploadSize(long maxUploadSize) { + this.fileUpload.setSizeMax(maxUploadSize); + } + + /** + * Set the maximum allowed size (in bytes) before uploads are written to disk. + * Uploaded files will still be received past this amount, but they will not be + * stored in memory. Default is 10240, according to Commons FileUpload. + * @param maxInMemorySize the maximum in memory size allowed + * @see org.apache.commons.fileupload.disk.DiskFileItemFactory#setSizeThreshold + */ + public void setMaxInMemorySize(int maxInMemorySize) { + this.fileItemFactory.setSizeThreshold(maxInMemorySize); + } + + /** + * Set the default character encoding to use for parsing requests, + * to be applied to headers of individual parts and to form fields. + * Default is ISO-8859-1, according to the Servlet spec. + *

If the request specifies a character encoding itself, the request + * encoding will override this setting. This also allows for generically + * overriding the character encoding in a filter that invokes the + * ServletRequest.setCharacterEncoding method. + * @param defaultEncoding the character encoding to use + * @see javax.servlet.ServletRequest#getCharacterEncoding + * @see javax.servlet.ServletRequest#setCharacterEncoding + * @see WebUtils#DEFAULT_CHARACTER_ENCODING + * @see org.apache.commons.fileupload.FileUploadBase#setHeaderEncoding + */ + public void setDefaultEncoding(String defaultEncoding) { + this.fileUpload.setHeaderEncoding(defaultEncoding); + } + + protected String getDefaultEncoding() { + String encoding = getFileUpload().getHeaderEncoding(); + if (encoding == null) { + encoding = WebUtils.DEFAULT_CHARACTER_ENCODING; + } + return encoding; + } + + /** + * Set the temporary directory where uploaded files get stored. + * Default is the servlet container's temporary directory for the web application. + * @see org.springframework.web.util.WebUtils#TEMP_DIR_CONTEXT_ATTRIBUTE + */ + public void setUploadTempDir(Resource uploadTempDir) throws IOException { + if (!uploadTempDir.exists() && !uploadTempDir.getFile().mkdirs()) { + throw new IllegalArgumentException("Given uploadTempDir [" + uploadTempDir + "] could not be created"); + } + this.fileItemFactory.setRepository(uploadTempDir.getFile()); + this.uploadTempDirSpecified = true; + } + + protected boolean isUploadTempDirSpecified() { + return this.uploadTempDirSpecified; + } + + + /** + * Factory method for a Commons DiskFileItemFactory instance. + *

Default implementation returns a standard DiskFileItemFactory. + * Can be overridden to use a custom subclass, e.g. for testing purposes. + * @return the new DiskFileItemFactory instance + */ + protected DiskFileItemFactory newFileItemFactory() { + return new DiskFileItemFactory(); + } + + /** + * Factory method for a Commons FileUpload instance. + *

To be implemented by subclasses. + * @param fileItemFactory the Commons FileItemFactory to build upon + * @return the Commons FileUpload instance + */ + protected abstract FileUpload newFileUpload(FileItemFactory fileItemFactory); + + + /** + * Determine an appropriate FileUpload instance for the given encoding. + *

Default implementation returns the shared FileUpload instance + * if the encoding matches, else creates a new FileUpload instance + * with the same configuration other than the desired encoding. + * @param encoding the character encoding to use + * @return an appropriate FileUpload instance. + */ + protected FileUpload prepareFileUpload(String encoding) { + FileUpload fileUpload = getFileUpload(); + FileUpload actualFileUpload = fileUpload; + + // Use new temporary FileUpload instance if the request specifies + // its own encoding that does not match the default encoding. + if (encoding != null && !encoding.equals(fileUpload.getHeaderEncoding())) { + actualFileUpload = newFileUpload(getFileItemFactory()); + actualFileUpload.setSizeMax(fileUpload.getSizeMax()); + actualFileUpload.setHeaderEncoding(encoding); + } + + return actualFileUpload; + } + + /** + * Parse the given List of Commons FileItems into a Spring MultipartParsingResult, + * containing Spring MultipartFile instances and a Map of multipart parameter. + * @param fileItems the Commons FileIterms to parse + * @param encoding the encoding to use for form fields + * @return the Spring MultipartParsingResult + * @see CommonsMultipartFile#CommonsMultipartFile(org.apache.commons.fileupload.FileItem) + */ + protected MultipartParsingResult parseFileItems(List fileItems, String encoding) { + Map multipartFiles = new HashMap(); + Map multipartParameters = new HashMap(); + + // Extract multipart files and multipart parameters. + for (Iterator it = fileItems.iterator(); it.hasNext();) { + FileItem fileItem = (FileItem) it.next(); + if (fileItem.isFormField()) { + String value = null; + if (encoding != null) { + try { + value = fileItem.getString(encoding); + } + catch (UnsupportedEncodingException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Could not decode multipart item '" + fileItem.getFieldName() + + "' with encoding '" + encoding + "': using platform default"); + } + value = fileItem.getString(); + } + } + else { + value = fileItem.getString(); + } + String[] curParam = (String[]) multipartParameters.get(fileItem.getFieldName()); + if (curParam == null) { + // simple form field + multipartParameters.put(fileItem.getFieldName(), new String[] { value }); + } + else { + // array of simple form fields + String[] newParam = StringUtils.addStringToArray(curParam, value); + multipartParameters.put(fileItem.getFieldName(), newParam); + } + } + else { + // multipart file field + CommonsMultipartFile file = new CommonsMultipartFile(fileItem); + if (multipartFiles.put(file.getName(), file) != null) { + throw new MultipartException( + "Multiple files for field name [" + file.getName() + "] found - not supported by MultipartResolver"); + } + if (logger.isDebugEnabled()) { + logger.debug("Found multipart file [" + file.getName() + "] of size " + file.getSize() + + " bytes with original filename [" + file.getOriginalFilename() + "], stored " + + file.getStorageDescription()); + } + } + } + return new MultipartParsingResult(multipartFiles, multipartParameters); + } + + /** + * Cleanup the Spring MultipartFiles created during multipart parsing, + * potentially holding temporary data on disk. + *

Deletes the underlying Commons FileItem instances. + * @param multipartFiles Collection of MultipartFile instances + * @see org.apache.commons.fileupload.FileItem#delete() + */ + protected void cleanupFileItems(Collection multipartFiles) { + for (Iterator it = multipartFiles.iterator(); it.hasNext();) { + CommonsMultipartFile file = (CommonsMultipartFile) it.next(); + if (logger.isDebugEnabled()) { + logger.debug("Cleaning up multipart file [" + file.getName() + "] with original filename [" + + file.getOriginalFilename() + "], stored " + file.getStorageDescription()); + } + file.getFileItem().delete(); + } + } + + + /** + * Holder for a Map of Spring MultipartFiles and a Map of + * multipart parameters. + */ + protected static class MultipartParsingResult { + + private final Map multipartFiles; + + private final Map multipartParameters; + + /** + * Create a new MultipartParsingResult. + * @param multipartFiles Map of field name to MultipartFile instance + * @param multipartParameters Map of field name to form field String value + */ + public MultipartParsingResult(Map multipartFiles, Map multipartParameters) { + this.multipartFiles = multipartFiles; + this.multipartParameters = multipartParameters; + } + + /** + * Return the multipart files as Map of field name to MultipartFile instance. + */ + public Map getMultipartFiles() { + return this.multipartFiles; + } + + /** + * Return the multipart parameters as Map of field name to form field String value. + */ + public Map getMultipartParameters() { + return this.multipartParameters; + } + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/commons/CommonsMultipartFile.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/commons/CommonsMultipartFile.java new file mode 100644 index 00000000000..8c042953eef --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/commons/CommonsMultipartFile.java @@ -0,0 +1,194 @@ +/* + * Copyright 2002-2006 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.web.multipart.commons; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.Serializable; + +import org.apache.commons.fileupload.FileItem; +import org.apache.commons.fileupload.FileUploadException; +import org.apache.commons.fileupload.disk.DiskFileItem; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.web.multipart.MultipartFile; + +/** + * MultipartFile implementation for Jakarta Commons FileUpload. + * + *

NOTE: As of Spring 2.0, this class requires Commons FileUpload 1.1 + * or higher. The implementation does not use any deprecated FileUpload 1.0 API + * anymore, to be compatible with future Commons FileUpload releases. + * + * @author Trevor D. Cook + * @author Juergen Hoeller + * @since 29.09.2003 + * @see CommonsMultipartResolver + */ +public class CommonsMultipartFile implements MultipartFile, Serializable { + + protected static final Log logger = LogFactory.getLog(CommonsMultipartFile.class); + + private final FileItem fileItem; + + private final long size; + + + /** + * Create an instance wrapping the given FileItem. + * @param fileItem the FileItem to wrap + */ + public CommonsMultipartFile(FileItem fileItem) { + this.fileItem = fileItem; + this.size = this.fileItem.getSize(); + } + + /** + * Return the underlying org.apache.commons.fileupload.FileItem + * instance. There is hardly any need to access this. + */ + public final FileItem getFileItem() { + return this.fileItem; + } + + + public String getName() { + return this.fileItem.getFieldName(); + } + + public String getOriginalFilename() { + String filename = this.fileItem.getName(); + if (filename == null) { + // Should never happen. + return ""; + } + // check for Unix-style path + int pos = filename.lastIndexOf("/"); + if (pos == -1) { + // check for Windows-style path + pos = filename.lastIndexOf("\\"); + } + if (pos != -1) { + // any sort of path separator found + return filename.substring(pos + 1); + } + else { + // plain name + return filename; + } + } + + public String getContentType() { + return this.fileItem.getContentType(); + } + + public boolean isEmpty() { + return (this.size == 0); + } + + public long getSize() { + return this.size; + } + + public byte[] getBytes() { + if (!isAvailable()) { + throw new IllegalStateException("File has been moved - cannot be read again"); + } + byte[] bytes = this.fileItem.get(); + return (bytes != null ? bytes : new byte[0]); + } + + public InputStream getInputStream() throws IOException { + if (!isAvailable()) { + throw new IllegalStateException("File has been moved - cannot be read again"); + } + InputStream inputStream = this.fileItem.getInputStream(); + return (inputStream != null ? inputStream : new ByteArrayInputStream(new byte[0])); + } + + public void transferTo(File dest) throws IOException, IllegalStateException { + if (!isAvailable()) { + throw new IllegalStateException("File has already been moved - cannot be transferred again"); + } + + if (dest.exists() && !dest.delete()) { + throw new IOException( + "Destination file [" + dest.getAbsolutePath() + "] already exists and could not be deleted"); + } + + try { + this.fileItem.write(dest); + if (logger.isDebugEnabled()) { + String action = "transferred"; + if (!this.fileItem.isInMemory()) { + action = isAvailable() ? "copied" : "moved"; + } + logger.debug("Multipart file '" + getName() + "' with original filename [" + + getOriginalFilename() + "], stored " + getStorageDescription() + ": " + + action + " to [" + dest.getAbsolutePath() + "]"); + } + } + catch (FileUploadException ex) { + throw new IllegalStateException(ex.getMessage()); + } + catch (IOException ex) { + throw ex; + } + catch (Exception ex) { + logger.error("Could not transfer to file", ex); + throw new IOException("Could not transfer to file: " + ex.getMessage()); + } + } + + /** + * Determine whether the multipart content is still available. + * If a temporary file has been moved, the content is no longer available. + */ + protected boolean isAvailable() { + // If in memory, it's available. + if (this.fileItem.isInMemory()) { + return true; + } + // Check actual existence of temporary file. + if (this.fileItem instanceof DiskFileItem) { + return ((DiskFileItem) this.fileItem).getStoreLocation().exists(); + } + // Check whether current file size is different than original one. + return (this.fileItem.getSize() == this.size); + } + + /** + * Return a description for the storage location of the multipart content. + * Tries to be as specific as possible: mentions the file location in case + * of a temporary file. + */ + public String getStorageDescription() { + if (this.fileItem.isInMemory()) { + return "in memory"; + } + else if (this.fileItem instanceof DiskFileItem) { + return "at [" + ((DiskFileItem) this.fileItem).getStoreLocation().getAbsolutePath() + "]"; + } + else { + return "on disk"; + } + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/commons/CommonsMultipartResolver.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/commons/CommonsMultipartResolver.java new file mode 100644 index 00000000000..918e30aece1 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/commons/CommonsMultipartResolver.java @@ -0,0 +1,205 @@ +/* + * Copyright 2002-2008 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.web.multipart.commons; + +import java.util.List; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.fileupload.FileItemFactory; +import org.apache.commons.fileupload.FileUpload; +import org.apache.commons.fileupload.FileUploadBase; +import org.apache.commons.fileupload.FileUploadException; +import org.apache.commons.fileupload.servlet.ServletFileUpload; +import org.apache.commons.fileupload.servlet.ServletRequestContext; + +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.web.context.ServletContextAware; +import org.springframework.web.multipart.MaxUploadSizeExceededException; +import org.springframework.web.multipart.MultipartException; +import org.springframework.web.multipart.MultipartHttpServletRequest; +import org.springframework.web.multipart.MultipartResolver; +import org.springframework.web.multipart.support.DefaultMultipartHttpServletRequest; +import org.springframework.web.util.WebUtils; + +/** + * Servlet-based {@link org.springframework.web.multipart.MultipartResolver} implementation + * for Jakarta Commons FileUpload + * 1.1 or above. Commons FileUpload 1.2 or above is recommended. + * + *

Provides "maxUploadSize", "maxInMemorySize" and "defaultEncoding" settings as + * bean properties (inherited from {@link CommonsFileUploadSupport}). See corresponding + * ServletFileUpload / DiskFileItemFactory properties ("sizeMax", "sizeThreshold", + * "headerEncoding") for details in terms of defaults and accepted values. + * + *

Saves temporary files to the servlet container's temporary directory. + * Needs to be initialized either by an application context or + * via the constructor that takes a ServletContext (for standalone usage). + * + * @author Trevor D. Cook + * @author Juergen Hoeller + * @since 29.09.2003 + * @see #CommonsMultipartResolver(ServletContext) + * @see #setResolveLazily + * @see org.springframework.web.portlet.multipart.CommonsPortletMultipartResolver + * @see org.apache.commons.fileupload.servlet.ServletFileUpload + * @see org.apache.commons.fileupload.disk.DiskFileItemFactory + */ +public class CommonsMultipartResolver extends CommonsFileUploadSupport + implements MultipartResolver, ServletContextAware { + + private final boolean commonsFileUpload12Present = + ClassUtils.hasMethod(ServletFileUpload.class, "isMultipartContent", new Class[] {HttpServletRequest.class}); + + private boolean resolveLazily = false; + + + /** + * Constructor for use as bean. Determines the servlet container's + * temporary directory via the ServletContext passed in as through the + * ServletContextAware interface (typically by a WebApplicationContext). + * @see #setServletContext + * @see org.springframework.web.context.ServletContextAware + * @see org.springframework.web.context.WebApplicationContext + */ + public CommonsMultipartResolver() { + super(); + } + + /** + * Constructor for standalone usage. Determines the servlet container's + * temporary directory via the given ServletContext. + * @param servletContext the ServletContext to use + */ + public CommonsMultipartResolver(ServletContext servletContext) { + this(); + setServletContext(servletContext); + } + + + /** + * Set whether to resolve the multipart request lazily at the time of + * file or parameter access. + *

Default is "false", resolving the multipart elements immediately, throwing + * corresponding exceptions at the time of the {@link #resolveMultipart} call. + * Switch this to "true" for lazy multipart parsing, throwing parse exceptions + * once the application attempts to obtain multipart files or parameters. + */ + public void setResolveLazily(boolean resolveLazily) { + this.resolveLazily = resolveLazily; + } + + /** + * Initialize the underlying org.apache.commons.fileupload.servlet.ServletFileUpload + * instance. Can be overridden to use a custom subclass, e.g. for testing purposes. + * @param fileItemFactory the Commons FileItemFactory to use + * @return the new ServletFileUpload instance + */ + protected FileUpload newFileUpload(FileItemFactory fileItemFactory) { + return new ServletFileUpload(fileItemFactory); + } + + public void setServletContext(ServletContext servletContext) { + if (!isUploadTempDirSpecified()) { + getFileItemFactory().setRepository(WebUtils.getTempDir(servletContext)); + } + } + + + public boolean isMultipart(HttpServletRequest request) { + if (request == null) { + return false; + } + else if (commonsFileUpload12Present) { + return ServletFileUpload.isMultipartContent(request); + } + else { + return ServletFileUpload.isMultipartContent(new ServletRequestContext(request)); + } + } + + public MultipartHttpServletRequest resolveMultipart(final HttpServletRequest request) throws MultipartException { + Assert.notNull(request, "Request must not be null"); + if (this.resolveLazily) { + return new DefaultMultipartHttpServletRequest(request) { + protected void initializeMultipart() { + MultipartParsingResult parsingResult = parseRequest(request); + setMultipartFiles(parsingResult.getMultipartFiles()); + setMultipartParameters(parsingResult.getMultipartParameters()); + } + }; + } + else { + MultipartParsingResult parsingResult = parseRequest(request); + return new DefaultMultipartHttpServletRequest( + request, parsingResult.getMultipartFiles(), parsingResult.getMultipartParameters()); + } + } + + /** + * Parse the given servlet request, resolving its multipart elements. + * @param request the request to parse + * @return the parsing result + * @throws MultipartException if multipart resolution failed. + */ + protected MultipartParsingResult parseRequest(HttpServletRequest request) throws MultipartException { + String encoding = determineEncoding(request); + FileUpload fileUpload = prepareFileUpload(encoding); + try { + List fileItems = ((ServletFileUpload) fileUpload).parseRequest(request); + return parseFileItems(fileItems, encoding); + } + catch (FileUploadBase.SizeLimitExceededException ex) { + throw new MaxUploadSizeExceededException(fileUpload.getSizeMax(), ex); + } + catch (FileUploadException ex) { + throw new MultipartException("Could not parse multipart servlet request", ex); + } + } + + /** + * Determine the encoding for the given request. + * Can be overridden in subclasses. + *

The default implementation checks the request encoding, + * falling back to the default encoding specified for this resolver. + * @param request current HTTP request + * @return the encoding for the request (never null) + * @see javax.servlet.ServletRequest#getCharacterEncoding + * @see #setDefaultEncoding + */ + protected String determineEncoding(HttpServletRequest request) { + String encoding = request.getCharacterEncoding(); + if (encoding == null) { + encoding = getDefaultEncoding(); + } + return encoding; + } + + public void cleanupMultipart(MultipartHttpServletRequest request) { + if (request != null) { + try { + cleanupFileItems(request.getFileMap().values()); + } + catch (Throwable ex) { + logger.warn("Failed to perform multipart cleanup for servlet request", ex); + } + } + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/commons/package.html b/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/commons/package.html new file mode 100644 index 00000000000..e7a033c0f7d --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/commons/package.html @@ -0,0 +1,8 @@ + + + +MultipartResolver implementation for +Jakarta Commons FileUpload. + + + diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/package.html b/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/package.html new file mode 100644 index 00000000000..fc91bd5a039 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/package.html @@ -0,0 +1,10 @@ + + + +Multipart resolution framework for handling file uploads. +Provides a MultipartResolver strategy interface, +and a generic extension of the HttpServletRequest interface +for accessing multipart files in web application code. + + + diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/support/AbstractMultipartHttpServletRequest.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/support/AbstractMultipartHttpServletRequest.java new file mode 100644 index 00000000000..53ef71c0770 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/support/AbstractMultipartHttpServletRequest.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2007 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.web.multipart.support; + +import java.util.Collections; +import java.util.Iterator; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; + +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartHttpServletRequest; + +/** + * Abstract base implementation of the MultipartHttpServletRequest interface. + * Provides management of pre-generated MultipartFile instances. + * + * @author Juergen Hoeller + * @since 06.10.2003 + */ +public abstract class AbstractMultipartHttpServletRequest extends HttpServletRequestWrapper + implements MultipartHttpServletRequest { + + private Map multipartFiles; + + + /** + * Wrap the given HttpServletRequest in a MultipartHttpServletRequest. + * @param request the request to wrap + */ + protected AbstractMultipartHttpServletRequest(HttpServletRequest request) { + super(request); + } + + + public Iterator getFileNames() { + return getMultipartFiles().keySet().iterator(); + } + + public MultipartFile getFile(String name) { + return (MultipartFile) getMultipartFiles().get(name); + } + + public Map getFileMap() { + return getMultipartFiles(); + } + + + /** + * Set a Map with parameter names as keys and MultipartFile objects as values. + * To be invoked by subclasses on initialization. + */ + protected final void setMultipartFiles(Map multipartFiles) { + this.multipartFiles = Collections.unmodifiableMap(multipartFiles); + } + + /** + * Obtain the MultipartFile Map for retrieval, + * lazily initializing it if necessary. + * @see #initializeMultipart() + */ + protected Map getMultipartFiles() { + if (this.multipartFiles == null) { + initializeMultipart(); + } + return this.multipartFiles; + } + + /** + * Lazily initialize the multipart request, if possible. + * Only called if not already eagerly initialized. + */ + protected void initializeMultipart() { + throw new IllegalStateException("Multipart request not initialized"); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/support/ByteArrayMultipartFileEditor.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/support/ByteArrayMultipartFileEditor.java new file mode 100644 index 00000000000..2e92c39f398 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/support/ByteArrayMultipartFileEditor.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2008 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.web.multipart.support; + +import java.io.IOException; + +import org.springframework.beans.propertyeditors.ByteArrayPropertyEditor; +import org.springframework.web.multipart.MultipartFile; + +/** + * Custom {@link java.beans.PropertyEditor} for converting + * {@link MultipartFile MultipartFiles} to byte arrays. + * + * @author Juergen Hoeller + * @since 13.10.2003 + */ +public class ByteArrayMultipartFileEditor extends ByteArrayPropertyEditor { + + public void setValue(Object value) { + if (value instanceof MultipartFile) { + MultipartFile multipartFile = (MultipartFile) value; + try { + super.setValue(multipartFile.getBytes()); + } + catch (IOException ex) { + IllegalArgumentException iae = new IllegalArgumentException("Cannot read contents of multipart file"); + iae.initCause(ex); + throw iae; + } + } + else if (value instanceof byte[]) { + super.setValue(value); + } + else { + super.setValue(value != null ? value.toString().getBytes() : null); + } + } + + public String getAsText() { + byte[] value = (byte[]) getValue(); + return (value != null ? new String(value) : ""); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/support/DefaultMultipartHttpServletRequest.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/support/DefaultMultipartHttpServletRequest.java new file mode 100644 index 00000000000..8838914be2e --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/support/DefaultMultipartHttpServletRequest.java @@ -0,0 +1,121 @@ +/* + * Copyright 2002-2007 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.web.multipart.support; + +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; + +/** + * Default implementation of the + * {@link org.springframework.web.multipart.MultipartHttpServletRequest} + * interface. Provides management of pre-generated parameter values. + * + * @author Trevor D. Cook + * @author Juergen Hoeller + * @since 29.09.2003 + * @see org.springframework.web.multipart.MultipartResolver + */ +public class DefaultMultipartHttpServletRequest extends AbstractMultipartHttpServletRequest { + + private Map multipartParameters; + + + /** + * Wrap the given HttpServletRequest in a MultipartHttpServletRequest. + * @param request the servlet request to wrap + * @param multipartFiles a map of the multipart files + * @param multipartParameters a map of the parameters to expose, + * with Strings as keys and String arrays as values + */ + public DefaultMultipartHttpServletRequest( + HttpServletRequest request, Map multipartFiles, Map multipartParameters) { + + super(request); + setMultipartFiles(multipartFiles); + setMultipartParameters(multipartParameters); + } + + /** + * Wrap the given HttpServletRequest in a MultipartHttpServletRequest. + * @param request the servlet request to wrap + */ + public DefaultMultipartHttpServletRequest(HttpServletRequest request) { + super(request); + } + + + public Enumeration getParameterNames() { + Set paramNames = new HashSet(); + Enumeration paramEnum = super.getParameterNames(); + while (paramEnum.hasMoreElements()) { + paramNames.add(paramEnum.nextElement()); + } + paramNames.addAll(getMultipartParameters().keySet()); + return Collections.enumeration(paramNames); + } + + public String getParameter(String name) { + String[] values = (String[]) getMultipartParameters().get(name); + if (values != null) { + return (values.length > 0 ? values[0] : null); + } + return super.getParameter(name); + } + + public String[] getParameterValues(String name) { + String[] values = (String[]) getMultipartParameters().get(name); + if (values != null) { + return values; + } + return super.getParameterValues(name); + } + + public Map getParameterMap() { + Map paramMap = new HashMap(); + paramMap.putAll(super.getParameterMap()); + paramMap.putAll(getMultipartParameters()); + return paramMap; + } + + + /** + * Set a Map with parameter names as keys and String array objects as values. + * To be invoked by subclasses on initialization. + */ + protected final void setMultipartParameters(Map multipartParameters) { + this.multipartParameters = multipartParameters; + } + + /** + * Obtain the multipart parameter Map for retrieval, + * lazily initializing it if necessary. + * @see #initializeMultipart() + */ + protected Map getMultipartParameters() { + if (this.multipartParameters == null) { + initializeMultipart(); + } + return this.multipartParameters; + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/support/MultipartFilter.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/support/MultipartFilter.java new file mode 100644 index 00000000000..349c83f8728 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/support/MultipartFilter.java @@ -0,0 +1,150 @@ +/* + * Copyright 2002-2006 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.web.multipart.support; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.WebApplicationContextUtils; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.multipart.MultipartHttpServletRequest; +import org.springframework.web.multipart.MultipartResolver; + +/** + * Servlet 2.3 Filter that resolves multipart requests via a MultipartResolver. + * in the root web application context. + * + *

Looks up the MultipartResolver in Spring's root web application context. + * Supports a "multipartResolverBeanName" filter init-param in web.xml; + * the default bean name is "filterMultipartResolver". Looks up the MultipartResolver + * on each request, to avoid initialization order issues (when using ContextLoaderServlet, + * the root application context will get initialized after this filter). + * + *

MultipartResolver lookup is customizable: Override this filter's + * lookupMultipartResolver method to use a custom MultipartResolver + * instance, for example if not using a Spring web application context. + * Note that the lookup method should not create a new MultipartResolver instance + * for each call but rather return a reference to a pre-built instance. + * + *

Note: This filter is an alternative to using DispatcherServlet's + * MultipartResolver support, for example for web applications with custom + * web views that do not use Spring's web MVC. It should not be combined with + * servlet-specific multipart resolution. + * + * @author Juergen Hoeller + * @since 08.10.2003 + * @see #setMultipartResolverBeanName + * @see #lookupMultipartResolver + * @see org.springframework.web.multipart.MultipartResolver + * @see org.springframework.web.servlet.DispatcherServlet + */ +public class MultipartFilter extends OncePerRequestFilter { + + public static final String DEFAULT_MULTIPART_RESOLVER_BEAN_NAME = "filterMultipartResolver"; + + private String multipartResolverBeanName = DEFAULT_MULTIPART_RESOLVER_BEAN_NAME; + + + /** + * Set the bean name of the MultipartResolver to fetch from Spring's + * root application context. Default is "filterMultipartResolver". + */ + public void setMultipartResolverBeanName(String multipartResolverBeanName) { + this.multipartResolverBeanName = multipartResolverBeanName; + } + + /** + * Return the bean name of the MultipartResolver to fetch from Spring's + * root application context. + */ + protected String getMultipartResolverBeanName() { + return multipartResolverBeanName; + } + + + /** + * Check for a multipart request via this filter's MultipartResolver, + * and wrap the original request with a MultipartHttpServletRequest if appropriate. + *

All later elements in the filter chain, most importantly servlets, benefit + * from proper parameter extraction in the multipart case, and are able to cast to + * MultipartHttpServletRequest if they need to. + */ + protected void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + MultipartResolver multipartResolver = lookupMultipartResolver(request); + + HttpServletRequest processedRequest = request; + if (multipartResolver.isMultipart(processedRequest)) { + if (logger.isDebugEnabled()) { + logger.debug("Resolving multipart request [" + processedRequest.getRequestURI() + + "] with MultipartFilter"); + } + processedRequest = multipartResolver.resolveMultipart(processedRequest); + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Request [" + processedRequest.getRequestURI() + "] is not a multipart request"); + } + } + + try { + filterChain.doFilter(processedRequest, response); + } + finally { + if (processedRequest instanceof MultipartHttpServletRequest) { + multipartResolver.cleanupMultipart((MultipartHttpServletRequest) processedRequest); + } + } + } + + /** + * Look up the MultipartResolver that this filter should use, + * taking the current HTTP request as argument. + *

Default implementation delegates to the lookupMultipartResolver + * without arguments. + * @return the MultipartResolver to use + * @see #lookupMultipartResolver() + */ + protected MultipartResolver lookupMultipartResolver(HttpServletRequest request) { + return lookupMultipartResolver(); + } + + /** + * Look for a MultipartResolver bean in the root web application context. + * Supports a "multipartResolverBeanName" filter init param; the default + * bean name is "filterMultipartResolver". + *

This can be overridden to use a custom MultipartResolver instance, + * for example if not using a Spring web application context. + * @return the MultipartResolver instance, or null if none found + */ + protected MultipartResolver lookupMultipartResolver() { + if (logger.isDebugEnabled()) { + logger.debug("Using MultipartResolver '" + getMultipartResolverBeanName() + "' for MultipartFilter"); + } + WebApplicationContext wac = + WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext()); + return (MultipartResolver) wac.getBean(getMultipartResolverBeanName(), MultipartResolver.class); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/support/StringMultipartFileEditor.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/support/StringMultipartFileEditor.java new file mode 100644 index 00000000000..ac99c7fb858 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/support/StringMultipartFileEditor.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2008 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.web.multipart.support; + +import java.beans.PropertyEditorSupport; +import java.io.IOException; + +import org.springframework.web.multipart.MultipartFile; + +/** + * Custom {@link java.beans.PropertyEditor} for converting + * {@link MultipartFile MultipartFiles} to Strings. + * + *

Allows one to specify the charset to use. + * + * @author Juergen Hoeller + * @since 13.10.2003 + */ +public class StringMultipartFileEditor extends PropertyEditorSupport { + + private final String charsetName; + + + /** + * Create a new {@link StringMultipartFileEditor}, using the default charset. + */ + public StringMultipartFileEditor() { + this.charsetName = null; + } + + /** + * Create a new {@link StringMultipartFileEditor}, using the given charset. + * @param charsetName valid charset name + * @see java.lang.String#String(byte[],String) + */ + public StringMultipartFileEditor(String charsetName) { + this.charsetName = charsetName; + } + + + public void setAsText(String text) { + setValue(text); + } + + public void setValue(Object value) { + if (value instanceof MultipartFile) { + MultipartFile multipartFile = (MultipartFile) value; + try { + super.setValue(this.charsetName != null ? + new String(multipartFile.getBytes(), this.charsetName) : + new String(multipartFile.getBytes())); + } + catch (IOException ex) { + IllegalArgumentException iae = new IllegalArgumentException("Cannot read contents of multipart file"); + iae.initCause(ex); + throw iae; + } + } + else { + super.setValue(value); + } + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/support/package.html b/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/support/package.html new file mode 100644 index 00000000000..b0b8dd82f7b --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/multipart/support/package.html @@ -0,0 +1,9 @@ + + + +Support classes for the multipart resolution framework. +Contains property editors for multipart files, and a +servlet filter for multipart handling without Spring's web MVC. + + + diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/DispatcherServlet.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/DispatcherServlet.java new file mode 100644 index 00000000000..8e3e61356de --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/DispatcherServlet.java @@ -0,0 +1,1294 @@ +/* + * Copyright 2002-2008 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.web.servlet; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.i18n.LocaleContext; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.core.JdkVersion; +import org.springframework.core.OrderComparator; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.support.PropertiesLoaderUtils; +import org.springframework.ui.context.ThemeSource; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.multipart.MultipartException; +import org.springframework.web.multipart.MultipartHttpServletRequest; +import org.springframework.web.multipart.MultipartResolver; +import org.springframework.web.util.NestedServletException; +import org.springframework.web.util.UrlPathHelper; +import org.springframework.web.util.WebUtils; + +/** + * Central dispatcher for HTTP request handlers/controllers, + * e.g. for web UI controllers or HTTP-based remote service exporters. + * Dispatches to registered handlers for processing a web request, + * providing convenient mapping and exception handling facilities. + * + *

This servlet is very flexible: It can be used with just about any workflow, + * with the installation of the appropriate adapter classes. It offers the + * following functionality that distinguishes it from other request-driven + * web MVC frameworks: + * + *

+ * + *

NOTE: The @RequestMapping annotation will only be processed + * if a corresponding HandlerMapping (for type level annotations) + * and/or HandlerAdapter (for method level annotations) + * is present in the dispatcher. This is the case by default. + * However, if you are defining custom HandlerMappings or + * HandlerAdapters, then you need to make sure that a + * corresponding custom DefaultAnnotationHandlerMapping + * and/or AnnotationMethodHandlerAdapter is defined as well + * - provided that you intend to use @RequestMapping. + * + *

A web application can define any number of DispatcherServlets. + * Each servlet will operate in its own namespace, loading its own application + * context with mappings, handlers, etc. Only the root application context + * as loaded by {@link org.springframework.web.context.ContextLoaderListener}, + * if any, will be shared. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Rob Harrop + * @see org.springframework.web.HttpRequestHandler + * @see org.springframework.web.servlet.mvc.Controller + * @see org.springframework.web.context.ContextLoaderListener + */ +public class DispatcherServlet extends FrameworkServlet { + + /** + * Well-known name for the MultipartResolver object in the bean factory for this namespace. + */ + public static final String MULTIPART_RESOLVER_BEAN_NAME = "multipartResolver"; + + /** + * Well-known name for the LocaleResolver object in the bean factory for this namespace. + */ + public static final String LOCALE_RESOLVER_BEAN_NAME = "localeResolver"; + + /** + * Well-known name for the ThemeResolver object in the bean factory for this namespace. + */ + public static final String THEME_RESOLVER_BEAN_NAME = "themeResolver"; + + /** + * Well-known name for the HandlerMapping object in the bean factory for this namespace. + * Only used when "detectAllHandlerMappings" is turned off. + * @see #setDetectAllHandlerMappings + */ + public static final String HANDLER_MAPPING_BEAN_NAME = "handlerMapping"; + + /** + * Well-known name for the HandlerAdapter object in the bean factory for this namespace. + * Only used when "detectAllHandlerAdapters" is turned off. + * @see #setDetectAllHandlerAdapters + */ + public static final String HANDLER_ADAPTER_BEAN_NAME = "handlerAdapter"; + + /** + * Well-known name for the HandlerExceptionResolver object in the bean factory for this + * namespace. Only used when "detectAllHandlerExceptionResolvers" is turned off. + * @see #setDetectAllHandlerExceptionResolvers + */ + public static final String HANDLER_EXCEPTION_RESOLVER_BEAN_NAME = "handlerExceptionResolver"; + + /** + * Well-known name for the RequestToViewNameTranslator object in the bean factory for + * this namespace. + */ + public static final String REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME = "viewNameTranslator"; + + /** + * Well-known name for the ViewResolver object in the bean factory for this namespace. + * Only used when "detectAllViewResolvers" is turned off. + * @see #setDetectAllViewResolvers + */ + public static final String VIEW_RESOLVER_BEAN_NAME = "viewResolver"; + + /** + * Request attribute to hold the currently chosen HandlerExecutionChain. + * Only used for internal optimizations. + */ + public static final String HANDLER_EXECUTION_CHAIN_ATTRIBUTE = DispatcherServlet.class.getName() + ".HANDLER"; + + /** + * Request attribute to hold the current web application context. + * Otherwise only the global web app context is obtainable by tags etc. + * @see org.springframework.web.servlet.support.RequestContextUtils#getWebApplicationContext + */ + public static final String WEB_APPLICATION_CONTEXT_ATTRIBUTE = DispatcherServlet.class.getName() + ".CONTEXT"; + + /** + * Request attribute to hold the current LocaleResolver, retrievable by views. + * @see org.springframework.web.servlet.support.RequestContextUtils#getLocaleResolver + */ + public static final String LOCALE_RESOLVER_ATTRIBUTE = DispatcherServlet.class.getName() + ".LOCALE_RESOLVER"; + + /** + * Request attribute to hold the current ThemeResolver, retrievable by views. + * @see org.springframework.web.servlet.support.RequestContextUtils#getThemeResolver + */ + public static final String THEME_RESOLVER_ATTRIBUTE = DispatcherServlet.class.getName() + ".THEME_RESOLVER"; + + /** + * Request attribute to hold the current ThemeSource, retrievable by views. + * @see org.springframework.web.servlet.support.RequestContextUtils#getThemeSource + */ + public static final String THEME_SOURCE_ATTRIBUTE = DispatcherServlet.class.getName() + ".THEME_SOURCE"; + + + /** + * Log category to use when no mapped handler is found for a request. + */ + public static final String PAGE_NOT_FOUND_LOG_CATEGORY = "org.springframework.web.servlet.PageNotFound"; + + /** + * Name of the class path resource (relative to the DispatcherServlet class) + * that defines DispatcherServlet's default strategy names. + */ + private static final String DEFAULT_STRATEGIES_PATH = "DispatcherServlet.properties"; + + + /** + * Additional logger to use when no mapped handler is found for a request. + */ + protected static final Log pageNotFoundLogger = LogFactory.getLog(PAGE_NOT_FOUND_LOG_CATEGORY); + + private static final Properties defaultStrategies; + + static { + // Load default strategy implementations from properties file. + // This is currently strictly internal and not meant to be customized + // by application developers. + try { + ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class); + defaultStrategies = PropertiesLoaderUtils.loadProperties(resource); + } + catch (IOException ex) { + throw new IllegalStateException("Could not load 'DispatcherServlet.properties': " + ex.getMessage()); + } + } + + + /** Detect all HandlerMappings or just expect "handlerMapping" bean? */ + private boolean detectAllHandlerMappings = true; + + /** Detect all HandlerAdapters or just expect "handlerAdapter" bean? */ + private boolean detectAllHandlerAdapters = true; + + /** Detect all HandlerExceptionResolvers or just expect "handlerExceptionResolver" bean? */ + private boolean detectAllHandlerExceptionResolvers = true; + + /** Detect all ViewResolvers or just expect "viewResolver" bean? */ + private boolean detectAllViewResolvers = true; + + /** Perform cleanup of request attributes after include request? */ + private boolean cleanupAfterInclude = true; + + /** Expose LocaleContext and RequestAttributes as inheritable for child threads? */ + private boolean threadContextInheritable = false; + + + /** MultipartResolver used by this servlet */ + private MultipartResolver multipartResolver; + + /** LocaleResolver used by this servlet */ + private LocaleResolver localeResolver; + + /** ThemeResolver used by this servlet */ + private ThemeResolver themeResolver; + + /** List of HandlerMappings used by this servlet */ + private List handlerMappings; + + /** List of HandlerAdapters used by this servlet */ + private List handlerAdapters; + + /** List of HandlerExceptionResolvers used by this servlet */ + private List handlerExceptionResolvers; + + /** RequestToViewNameTranslator used by this servlet */ + private RequestToViewNameTranslator viewNameTranslator; + + /** List of ViewResolvers used by this servlet */ + private List viewResolvers; + + + /** + * Set whether to detect all HandlerMapping beans in this servlet's context. + * Else, just a single bean with name "handlerMapping" will be expected. + *

Default is "true". Turn this off if you want this servlet to use a + * single HandlerMapping, despite multiple HandlerMapping beans being + * defined in the context. + */ + public void setDetectAllHandlerMappings(boolean detectAllHandlerMappings) { + this.detectAllHandlerMappings = detectAllHandlerMappings; + } + + /** + * Set whether to detect all HandlerAdapter beans in this servlet's context. + * Else, just a single bean with name "handlerAdapter" will be expected. + *

Default is "true". Turn this off if you want this servlet to use a + * single HandlerAdapter, despite multiple HandlerAdapter beans being + * defined in the context. + */ + public void setDetectAllHandlerAdapters(boolean detectAllHandlerAdapters) { + this.detectAllHandlerAdapters = detectAllHandlerAdapters; + } + + /** + * Set whether to detect all HandlerExceptionResolver beans in this servlet's context. + * Else, just a single bean with name "handlerExceptionResolver" will be expected. + *

Default is "true". Turn this off if you want this servlet to use a + * single HandlerExceptionResolver, despite multiple HandlerExceptionResolver + * beans being defined in the context. + */ + public void setDetectAllHandlerExceptionResolvers(boolean detectAllHandlerExceptionResolvers) { + this.detectAllHandlerExceptionResolvers = detectAllHandlerExceptionResolvers; + } + + /** + * Set whether to detect all ViewResolver beans in this servlet's context. + * Else, just a single bean with name "viewResolver" will be expected. + *

Default is "true". Turn this off if you want this servlet to use a + * single ViewResolver, despite multiple ViewResolver beans being + * defined in the context. + */ + public void setDetectAllViewResolvers(boolean detectAllViewResolvers) { + this.detectAllViewResolvers = detectAllViewResolvers; + } + + /** + * Set whether to perform cleanup of request attributes after an include request, + * that is, whether to reset the original state of all request attributes after + * the DispatcherServlet has processed within an include request. Else, just the + * DispatcherServlet's own request attributes will be reset, but not model + * attributes for JSPs or special attributes set by views (for example, JSTL's). + *

Default is "true", which is strongly recommended. Views should not rely on + * request attributes having been set by (dynamic) includes. This allows JSP views + * rendered by an included controller to use any model attributes, even with the + * same names as in the main JSP, without causing side effects. Only turn this + * off for special needs, for example to deliberately allow main JSPs to access + * attributes from JSP views rendered by an included controller. + */ + public void setCleanupAfterInclude(boolean cleanupAfterInclude) { + this.cleanupAfterInclude = cleanupAfterInclude; + } + + /** + * Set whether to expose the LocaleContext and RequestAttributes as inheritable + * for child threads (using an {@link java.lang.InheritableThreadLocal}). + *

Default is "false", to avoid side effects on spawned background threads. + * Switch this to "true" to enable inheritance for custom child threads which + * are spawned during request processing and only used for this request + * (that is, ending after their initial task, without reuse of the thread). + *

WARNING: Do not use inheritance for child threads if you are + * accessing a thread pool which is configured to potentially add new threads + * on demand (e.g. a JDK {@link java.util.concurrent.ThreadPoolExecutor}), + * since this will expose the inherited context to such a pooled thread. + */ + public void setThreadContextInheritable(boolean threadContextInheritable) { + this.threadContextInheritable = threadContextInheritable; + } + + + /** + * This implementation calls {@link #initStrategies}. + */ + protected void onRefresh(ApplicationContext context) throws BeansException { + initStrategies(context); + } + + /** + * Initialize the strategy objects that this servlet uses. + *

May be overridden in subclasses in order to initialize + * further strategy objects. + */ + protected void initStrategies(ApplicationContext context) { + initMultipartResolver(context); + initLocaleResolver(context); + initThemeResolver(context); + initHandlerMappings(context); + initHandlerAdapters(context); + initHandlerExceptionResolvers(context); + initRequestToViewNameTranslator(context); + initViewResolvers(context); + } + + /** + * Initialize the MultipartResolver used by this class. + *

If no bean is defined with the given name in the BeanFactory + * for this namespace, no multipart handling is provided. + */ + private void initMultipartResolver(ApplicationContext context) { + try { + this.multipartResolver = (MultipartResolver) + context.getBean(MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class); + if (logger.isDebugEnabled()) { + logger.debug("Using MultipartResolver [" + this.multipartResolver + "]"); + } + } + catch (NoSuchBeanDefinitionException ex) { + // Default is no multipart resolver. + this.multipartResolver = null; + if (logger.isDebugEnabled()) { + logger.debug("Unable to locate MultipartResolver with name '" + MULTIPART_RESOLVER_BEAN_NAME + + "': no multipart request handling provided"); + } + } + } + + /** + * Initialize the LocaleResolver used by this class. + *

If no bean is defined with the given name in the BeanFactory + * for this namespace, we default to AcceptHeaderLocaleResolver. + */ + private void initLocaleResolver(ApplicationContext context) { + try { + this.localeResolver = (LocaleResolver) + context.getBean(LOCALE_RESOLVER_BEAN_NAME, LocaleResolver.class); + if (logger.isDebugEnabled()) { + logger.debug("Using LocaleResolver [" + this.localeResolver + "]"); + } + } + catch (NoSuchBeanDefinitionException ex) { + // We need to use the default. + this.localeResolver = (LocaleResolver) getDefaultStrategy(context, LocaleResolver.class); + if (logger.isDebugEnabled()) { + logger.debug("Unable to locate LocaleResolver with name '" + LOCALE_RESOLVER_BEAN_NAME + + "': using default [" + this.localeResolver + "]"); + } + } + } + + /** + * Initialize the ThemeResolver used by this class. + *

If no bean is defined with the given name in the BeanFactory + * for this namespace, we default to a FixedThemeResolver. + */ + private void initThemeResolver(ApplicationContext context) { + try { + this.themeResolver = (ThemeResolver) + context.getBean(THEME_RESOLVER_BEAN_NAME, ThemeResolver.class); + if (logger.isDebugEnabled()) { + logger.debug("Using ThemeResolver [" + this.themeResolver + "]"); + } + } + catch (NoSuchBeanDefinitionException ex) { + // We need to use the default. + this.themeResolver = (ThemeResolver) getDefaultStrategy(context, ThemeResolver.class); + if (logger.isDebugEnabled()) { + logger.debug("Unable to locate ThemeResolver with name '" + THEME_RESOLVER_BEAN_NAME + + "': using default [" + this.themeResolver + "]"); + } + } + } + + /** + * Initialize the HandlerMappings used by this class. + *

If no HandlerMapping beans are defined in the BeanFactory + * for this namespace, we default to BeanNameUrlHandlerMapping. + */ + private void initHandlerMappings(ApplicationContext context) { + this.handlerMappings = null; + + if (this.detectAllHandlerMappings) { + // Find all HandlerMappings in the ApplicationContext, + // including ancestor contexts. + Map matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors( + context, HandlerMapping.class, true, false); + if (!matchingBeans.isEmpty()) { + this.handlerMappings = new ArrayList(matchingBeans.values()); + // We keep HandlerMappings in sorted order. + Collections.sort(this.handlerMappings, new OrderComparator()); + } + } + else { + try { + Object hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class); + this.handlerMappings = Collections.singletonList(hm); + } + catch (NoSuchBeanDefinitionException ex) { + // Ignore, we'll add a default HandlerMapping later. + } + } + + // Ensure we have at least one HandlerMapping, by registering + // a default HandlerMapping if no other mappings are found. + if (this.handlerMappings == null) { + this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class); + if (logger.isDebugEnabled()) { + logger.debug("No HandlerMappings found in servlet '" + getServletName() + "': using default"); + } + } + } + + /** + * Initialize the HandlerAdapters used by this class. + *

If no HandlerAdapter beans are defined in the BeanFactory + * for this namespace, we default to SimpleControllerHandlerAdapter. + */ + private void initHandlerAdapters(ApplicationContext context) { + this.handlerAdapters = null; + + if (this.detectAllHandlerAdapters) { + // Find all HandlerAdapters in the ApplicationContext, + // including ancestor contexts. + Map matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors( + context, HandlerAdapter.class, true, false); + if (!matchingBeans.isEmpty()) { + this.handlerAdapters = new ArrayList(matchingBeans.values()); + // We keep HandlerAdapters in sorted order. + Collections.sort(this.handlerAdapters, new OrderComparator()); + } + } + else { + try { + Object ha = context.getBean(HANDLER_ADAPTER_BEAN_NAME, HandlerAdapter.class); + this.handlerAdapters = Collections.singletonList(ha); + } + catch (NoSuchBeanDefinitionException ex) { + // Ignore, we'll add a default HandlerAdapter later. + } + } + + // Ensure we have at least some HandlerAdapters, by registering + // default HandlerAdapters if no other adapters are found. + if (this.handlerAdapters == null) { + this.handlerAdapters = getDefaultStrategies(context, HandlerAdapter.class); + if (logger.isDebugEnabled()) { + logger.debug("No HandlerAdapters found in servlet '" + getServletName() + "': using default"); + } + } + } + + /** + * Initialize the HandlerExceptionResolver used by this class. + *

If no bean is defined with the given name in the BeanFactory + * for this namespace, we default to no exception resolver. + */ + private void initHandlerExceptionResolvers(ApplicationContext context) { + this.handlerExceptionResolvers = null; + + if (this.detectAllHandlerExceptionResolvers) { + // Find all HandlerExceptionResolvers in the ApplicationContext, + // including ancestor contexts. + Map matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors( + context, HandlerExceptionResolver.class, true, false); + if (!matchingBeans.isEmpty()) { + this.handlerExceptionResolvers = new ArrayList(matchingBeans.values()); + // We keep HandlerExceptionResolvers in sorted order. + Collections.sort(this.handlerExceptionResolvers, new OrderComparator()); + } + } + else { + try { + Object her = context.getBean( + HANDLER_EXCEPTION_RESOLVER_BEAN_NAME, HandlerExceptionResolver.class); + this.handlerExceptionResolvers = Collections.singletonList(her); + } + catch (NoSuchBeanDefinitionException ex) { + // Ignore, no HandlerExceptionResolver is fine too. + } + } + + // Just for consistency, check for default HandlerExceptionResolvers... + // There aren't any in usual scenarios. + if (this.handlerExceptionResolvers == null) { + this.handlerExceptionResolvers = getDefaultStrategies(context, HandlerExceptionResolver.class); + if (logger.isDebugEnabled()) { + logger.debug("No HandlerExceptionResolvers found in servlet '" + getServletName() + "': using default"); + } + } + } + + /** + * Initialize the RequestToViewNameTranslator used by this servlet instance. If no + * implementation is configured then we default to DefaultRequestToViewNameTranslator. + */ + private void initRequestToViewNameTranslator(ApplicationContext context) { + try { + this.viewNameTranslator = (RequestToViewNameTranslator) context.getBean( + REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME, RequestToViewNameTranslator.class); + if (logger.isDebugEnabled()) { + logger.debug("Using RequestToViewNameTranslator [" + this.viewNameTranslator + "]"); + } + } + catch (NoSuchBeanDefinitionException ex) { + // We need to use the default. + this.viewNameTranslator = + (RequestToViewNameTranslator) getDefaultStrategy(context, RequestToViewNameTranslator.class); + if (logger.isDebugEnabled()) { + logger.debug("Unable to locate RequestToViewNameTranslator with name '" + + REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME + + "': using default [" + this.viewNameTranslator + "]"); + } + } + } + + /** + * Initialize the ViewResolvers used by this class. + *

If no ViewResolver beans are defined in the BeanFactory + * for this namespace, we default to InternalResourceViewResolver. + */ + private void initViewResolvers(ApplicationContext context) { + this.viewResolvers = null; + + if (this.detectAllViewResolvers) { + // Find all ViewResolvers in the ApplicationContext, + // including ancestor contexts. + Map matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors( + context, ViewResolver.class, true, false); + if (!matchingBeans.isEmpty()) { + this.viewResolvers = new ArrayList(matchingBeans.values()); + // We keep ViewResolvers in sorted order. + Collections.sort(this.viewResolvers, new OrderComparator()); + } + } + else { + try { + Object vr = context.getBean(VIEW_RESOLVER_BEAN_NAME, ViewResolver.class); + this.viewResolvers = Collections.singletonList(vr); + } + catch (NoSuchBeanDefinitionException ex) { + // Ignore, we'll add a default ViewResolver later. + } + } + + // Ensure we have at least one ViewResolver, by registering + // a default ViewResolver if no other resolvers are found. + if (this.viewResolvers == null) { + this.viewResolvers = getDefaultStrategies(context, ViewResolver.class); + if (logger.isDebugEnabled()) { + logger.debug("No ViewResolvers found in servlet '" + getServletName() + "': using default"); + } + } + } + + /** + * Return this servlet's ThemeSource, if any; else return null. + *

Default is to return the WebApplicationContext as ThemeSource, + * provided that it implements the ThemeSource interface. + * @return the ThemeSource, if any + * @see #getWebApplicationContext() + */ + public final ThemeSource getThemeSource() { + if (getWebApplicationContext() instanceof ThemeSource) { + return (ThemeSource) getWebApplicationContext(); + } + else { + return null; + } + } + + /** + * Obtain this servlet's MultipartResolver, if any. + * @return the MultipartResolver used by this servlet, or null + * if none (indicating that no multipart support is available) + */ + public final MultipartResolver getMultipartResolver() { + return this.multipartResolver; + } + + + /** + * Return the default strategy object for the given strategy interface. + *

The default implementation delegates to {@link #getDefaultStrategies}, + * expecting a single object in the list. + * @param context the current WebApplicationContext + * @param strategyInterface the strategy interface + * @return the corresponding strategy object + * @throws BeansException if initialization failed + * @see #getDefaultStrategies + */ + protected Object getDefaultStrategy(ApplicationContext context, Class strategyInterface) throws BeansException { + List strategies = getDefaultStrategies(context, strategyInterface); + if (strategies.size() != 1) { + throw new BeanInitializationException( + "DispatcherServlet needs exactly 1 strategy for interface [" + strategyInterface.getName() + "]"); + } + return strategies.get(0); + } + + /** + * Create a List of default strategy objects for the given strategy interface. + *

The default implementation uses the "DispatcherServlet.properties" file + * (in the same package as the DispatcherServlet class) to determine the class names. + * It instantiates the strategy objects through the context's BeanFactory. + * @param context the current WebApplicationContext + * @param strategyInterface the strategy interface + * @return the List of corresponding strategy objects + * @throws BeansException if initialization failed + */ + protected List getDefaultStrategies(ApplicationContext context, Class strategyInterface) throws BeansException { + String key = strategyInterface.getName(); + List strategies = null; + String value = defaultStrategies.getProperty(key); + if (value != null) { + String[] classNames = StringUtils.commaDelimitedListToStringArray(value); + strategies = new ArrayList(classNames.length); + for (int i = 0; i < classNames.length; i++) { + String className = classNames[i]; + if (JdkVersion.getMajorJavaVersion() < JdkVersion.JAVA_15 && className.indexOf("Annotation") != -1) { + // Skip Java 5 specific strategies when running on JDK 1.4... + continue; + } + try { + Class clazz = ClassUtils.forName(className, DispatcherServlet.class.getClassLoader()); + Object strategy = createDefaultStrategy(context, clazz); + strategies.add(strategy); + } + catch (ClassNotFoundException ex) { + throw new BeanInitializationException( + "Could not find DispatcherServlet's default strategy class [" + className + + "] for interface [" + key + "]", ex); + } + catch (LinkageError err) { + throw new BeanInitializationException( + "Error loading DispatcherServlet's default strategy class [" + className + + "] for interface [" + key + "]: problem with class file or dependent class", err); + } + } + } + else { + strategies = Collections.EMPTY_LIST; + } + return strategies; + } + + /** + * Create a default strategy. + *

The default implementation uses + * {@link org.springframework.beans.factory.config.AutowireCapableBeanFactory#createBean}. + * @param context the current WebApplicationContext + * @param clazz the strategy implementation class to instantiate + * @throws BeansException if initialization failed + * @return the fully configured strategy instance + * @see org.springframework.context.ApplicationContext#getAutowireCapableBeanFactory() + * @see org.springframework.beans.factory.config.AutowireCapableBeanFactory#createBean + */ + protected Object createDefaultStrategy(ApplicationContext context, Class clazz) throws BeansException { + return context.getAutowireCapableBeanFactory().createBean(clazz); + } + + + /** + * Exposes the DispatcherServlet-specific request attributes and + * delegates to {@link #doDispatch} for the actual dispatching. + */ + protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception { + if (logger.isDebugEnabled()) { + String requestUri = new UrlPathHelper().getRequestUri(request); + logger.debug("DispatcherServlet with name '" + getServletName() + + "' processing request for [" + requestUri + "]"); + } + + // Keep a snapshot of the request attributes in case of an include, + // to be able to restore the original attributes after the include. + Map attributesSnapshot = null; + if (WebUtils.isIncludeRequest(request)) { + logger.debug("Taking snapshot of request attributes before include"); + attributesSnapshot = new HashMap(); + Enumeration attrNames = request.getAttributeNames(); + while (attrNames.hasMoreElements()) { + String attrName = (String) attrNames.nextElement(); + if (this.cleanupAfterInclude || attrName.startsWith("org.springframework.web.servlet")) { + attributesSnapshot.put(attrName, request.getAttribute(attrName)); + } + } + } + + // Make framework objects available to handlers and view objects. + request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext()); + request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver); + request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver); + request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource()); + + try { + doDispatch(request, response); + } + finally { + // Restore the original attribute snapshot, in case of an include. + if (attributesSnapshot != null) { + restoreAttributesAfterInclude(request, attributesSnapshot); + } + } + } + + /** + * Process the actual dispatching to the handler. + *

The handler will be obtained by applying the servlet's HandlerMappings in order. + * The HandlerAdapter will be obtained by querying the servlet's installed + * HandlerAdapters to find the first that supports the handler class. + *

All HTTP methods are handled by this method. It's up to HandlerAdapters or + * handlers themselves to decide which methods are acceptable. + * @param request current HTTP request + * @param response current HTTP response + * @throws Exception in case of any kind of processing failure + */ + protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { + HttpServletRequest processedRequest = request; + HandlerExecutionChain mappedHandler = null; + int interceptorIndex = -1; + + // Expose current LocaleResolver and request as LocaleContext. + LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext(); + LocaleContextHolder.setLocaleContext(buildLocaleContext(request), this.threadContextInheritable); + + // Expose current RequestAttributes to current thread. + RequestAttributes previousRequestAttributes = RequestContextHolder.getRequestAttributes(); + ServletRequestAttributes requestAttributes = new ServletRequestAttributes(request); + RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable); + + if (logger.isTraceEnabled()) { + logger.trace("Bound request context to thread: " + request); + } + + try { + ModelAndView mv = null; + boolean errorView = false; + + try { + processedRequest = checkMultipart(request); + + // Determine handler for the current request. + mappedHandler = getHandler(processedRequest, false); + if (mappedHandler == null || mappedHandler.getHandler() == null) { + noHandlerFound(processedRequest, response); + return; + } + + // Apply preHandle methods of registered interceptors. + HandlerInterceptor[] interceptors = mappedHandler.getInterceptors(); + if (interceptors != null) { + for (int i = 0; i < interceptors.length; i++) { + HandlerInterceptor interceptor = interceptors[i]; + if (!interceptor.preHandle(processedRequest, response, mappedHandler.getHandler())) { + triggerAfterCompletion(mappedHandler, interceptorIndex, processedRequest, response, null); + return; + } + interceptorIndex = i; + } + } + + // Actually invoke the handler. + HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); + mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); + + // Do we need view name translation? + if (mv != null && !mv.hasView()) { + mv.setViewName(getDefaultViewName(request)); + } + + // Apply postHandle methods of registered interceptors. + if (interceptors != null) { + for (int i = interceptors.length - 1; i >= 0; i--) { + HandlerInterceptor interceptor = interceptors[i]; + interceptor.postHandle(processedRequest, response, mappedHandler.getHandler(), mv); + } + } + } + catch (ModelAndViewDefiningException ex) { + logger.debug("ModelAndViewDefiningException encountered", ex); + mv = ex.getModelAndView(); + } + catch (Exception ex) { + Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null); + mv = processHandlerException(processedRequest, response, handler, ex); + errorView = (mv != null); + } + + // Did the handler return a view to render? + if (mv != null && !mv.wasCleared()) { + render(mv, processedRequest, response); + if (errorView) { + WebUtils.clearErrorRequestAttributes(request); + } + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Null ModelAndView returned to DispatcherServlet with name '" + + getServletName() + "': assuming HandlerAdapter completed request handling"); + } + } + + // Trigger after-completion for successful outcome. + triggerAfterCompletion(mappedHandler, interceptorIndex, processedRequest, response, null); + } + + catch (Exception ex) { + // Trigger after-completion for thrown exception. + triggerAfterCompletion(mappedHandler, interceptorIndex, processedRequest, response, ex); + throw ex; + } + catch (Error err) { + ServletException ex = new NestedServletException("Handler processing failed", err); + // Trigger after-completion for thrown exception. + triggerAfterCompletion(mappedHandler, interceptorIndex, processedRequest, response, ex); + throw ex; + } + + finally { + // Clean up any resources used by a multipart request. + if (processedRequest != request) { + cleanupMultipart(processedRequest); + } + + // Reset thread-bound context. + RequestContextHolder.setRequestAttributes(previousRequestAttributes, this.threadContextInheritable); + LocaleContextHolder.setLocaleContext(previousLocaleContext, this.threadContextInheritable); + + // Clear request attributes. + requestAttributes.requestCompleted(); + if (logger.isTraceEnabled()) { + logger.trace("Cleared thread-bound request context: " + request); + } + } + } + + /** + * Override HttpServlet's getLastModified method to evaluate + * the Last-Modified value of the mapped handler. + */ + protected long getLastModified(HttpServletRequest request) { + if (logger.isDebugEnabled()) { + String requestUri = new UrlPathHelper().getRequestUri(request); + logger.debug("DispatcherServlet with name '" + getServletName() + + "' determining Last-Modified value for [" + requestUri + "]"); + } + try { + HandlerExecutionChain mappedHandler = getHandler(request, true); + if (mappedHandler == null || mappedHandler.getHandler() == null) { + // Ignore -> will reappear on doService. + logger.debug("No handler found in getLastModified"); + return -1; + } + + HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); + long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); + if (logger.isDebugEnabled()) { + String requestUri = new UrlPathHelper().getRequestUri(request); + logger.debug("Last-Modified value for [" + requestUri + "] is: " + lastModified); + } + return lastModified; + } + catch (Exception ex) { + // Ignore -> will reappear on doService. + logger.debug("Exception thrown in getLastModified", ex); + return -1; + } + } + + + /** + * Build a LocaleContext for the given request, exposing the request's + * primary locale as current locale. + *

The default implementation uses the dispatcher's LocaleResolver + * to obtain the current locale, which might change during a request. + * @param request current HTTP request + * @return the corresponding LocaleContext + */ + protected LocaleContext buildLocaleContext(final HttpServletRequest request) { + return new LocaleContext() { + public Locale getLocale() { + return localeResolver.resolveLocale(request); + } + public String toString() { + return getLocale().toString(); + } + }; + } + + /** + * Convert the request into a multipart request, and make multipart resolver available. + * If no multipart resolver is set, simply use the existing request. + * @param request current HTTP request + * @return the processed request (multipart wrapper if necessary) + * @see MultipartResolver#resolveMultipart + */ + protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException { + if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) { + if (request instanceof MultipartHttpServletRequest) { + logger.debug("Request is already a MultipartHttpServletRequest - if not in a forward, " + + "this typically results from an additional MultipartFilter in web.xml"); + } + else { + return this.multipartResolver.resolveMultipart(request); + } + } + // If not returned before: return original request. + return request; + } + + /** + * Clean up any resources used by the given multipart request (if any). + * @param request current HTTP request + * @see MultipartResolver#cleanupMultipart + */ + protected void cleanupMultipart(HttpServletRequest request) { + if (request instanceof MultipartHttpServletRequest) { + this.multipartResolver.cleanupMultipart((MultipartHttpServletRequest) request); + } + } + + /** + * Return the HandlerExecutionChain for this request. + * Try all handler mappings in order. + * @param request current HTTP request + * @param cache whether to cache the HandlerExecutionChain in a request attribute + * @return the HandlerExceutionChain, or null if no handler could be found + */ + protected HandlerExecutionChain getHandler(HttpServletRequest request, boolean cache) throws Exception { + HandlerExecutionChain handler = + (HandlerExecutionChain) request.getAttribute(HANDLER_EXECUTION_CHAIN_ATTRIBUTE); + if (handler != null) { + if (!cache) { + request.removeAttribute(HANDLER_EXECUTION_CHAIN_ATTRIBUTE); + } + return handler; + } + + Iterator it = this.handlerMappings.iterator(); + while (it.hasNext()) { + HandlerMapping hm = (HandlerMapping) it.next(); + if (logger.isTraceEnabled()) { + logger.trace("Testing handler map [" + hm + "] in DispatcherServlet with name '" + + getServletName() + "'"); + } + handler = hm.getHandler(request); + if (handler != null) { + if (cache) { + request.setAttribute(HANDLER_EXECUTION_CHAIN_ATTRIBUTE, handler); + } + return handler; + } + } + return null; + } + + /** + * No handler found -> set appropriate HTTP response status. + * @param request current HTTP request + * @param response current HTTP response + * @throws Exception if preparing the response failed + */ + protected void noHandlerFound(HttpServletRequest request, HttpServletResponse response) throws Exception { + if (pageNotFoundLogger.isWarnEnabled()) { + String requestUri = new UrlPathHelper().getRequestUri(request); + pageNotFoundLogger.warn("No mapping found for HTTP request with URI [" + + requestUri + "] in DispatcherServlet with name '" + getServletName() + "'"); + } + response.sendError(HttpServletResponse.SC_NOT_FOUND); + } + + /** + * Return the HandlerAdapter for this handler object. + * @param handler the handler object to find an adapter for + * @throws ServletException if no HandlerAdapter can be found for the handler. + * This is a fatal error. + */ + protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException { + Iterator it = this.handlerAdapters.iterator(); + while (it.hasNext()) { + HandlerAdapter ha = (HandlerAdapter) it.next(); + if (logger.isTraceEnabled()) { + logger.trace("Testing handler adapter [" + ha + "]"); + } + if (ha.supports(handler)) { + return ha; + } + } + throw new ServletException("No adapter for handler [" + handler + + "]: Does your handler implement a supported interface like Controller?"); + } + + /** + * Determine an error ModelAndView via the registered HandlerExceptionResolvers. + * @param request current HTTP request + * @param response current HTTP response + * @param handler the executed handler, or null if none chosen at the time of + * the exception (for example, if multipart resolution failed) + * @param ex the exception that got thrown during handler execution + * @return a corresponding ModelAndView to forward to + * @throws Exception if no error ModelAndView found + */ + protected ModelAndView processHandlerException( + HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) + throws Exception { + + // Check registerer HandlerExceptionResolvers... + ModelAndView exMv = null; + for (Iterator it = this.handlerExceptionResolvers.iterator(); exMv == null && it.hasNext();) { + HandlerExceptionResolver resolver = (HandlerExceptionResolver) it.next(); + exMv = resolver.resolveException(request, response, handler, ex); + } + if (exMv != null) { + if (logger.isDebugEnabled()) { + logger.debug("Handler execution resulted in exception - forwarding to resolved error view: " + exMv, ex); + } + WebUtils.exposeErrorRequestAttributes(request, ex, getServletName()); + return exMv; + } + + // Send default responses for well-known exceptions, if possible. + if (ex instanceof HttpRequestMethodNotSupportedException && !response.isCommitted()) { + String[] supportedMethods = ((HttpRequestMethodNotSupportedException) ex).getSupportedMethods(); + if (supportedMethods != null) { + response.setHeader("Allow", StringUtils.arrayToDelimitedString(supportedMethods, ", ")); + } + response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, ex.getMessage()); + return null; + } + + throw ex; + } + + /** + * Render the given ModelAndView. This is the last stage in handling a request. + * It may involve resolving the view by name. + * @param mv the ModelAndView to render + * @param request current HTTP servlet request + * @param response current HTTP servlet response + * @throws Exception if there's a problem rendering the view + */ + protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) + throws Exception { + + // Determine locale for request and apply it to the response. + Locale locale = this.localeResolver.resolveLocale(request); + response.setLocale(locale); + + View view = null; + + if (mv.isReference()) { + // We need to resolve the view name. + view = resolveViewName(mv.getViewName(), mv.getModelInternal(), locale, request); + if (view == null) { + throw new ServletException("Could not resolve view with name '" + mv.getViewName() + + "' in servlet with name '" + getServletName() + "'"); + } + } + else { + // No need to lookup: the ModelAndView object contains the actual View object. + view = mv.getView(); + if (view == null) { + throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " + + "View object in servlet with name '" + getServletName() + "'"); + } + } + + // Delegate to the View object for rendering. + if (logger.isDebugEnabled()) { + logger.debug("Rendering view [" + view + "] in DispatcherServlet with name '" + getServletName() + "'"); + } + view.render(mv.getModelInternal(), request, response); + } + + /** + * Translate the supplied request into a default view name. + * @param request current HTTP servlet request + * @return the view name (or null if no default found) + * @throws Exception if view name translation failed + */ + protected String getDefaultViewName(HttpServletRequest request) throws Exception { + return this.viewNameTranslator.getViewName(request); + } + + /** + * Resolve the given view name into a View object (to be rendered). + *

Default implementations asks all ViewResolvers of this dispatcher. + * Can be overridden for custom resolution strategies, potentially based + * on specific model attributes or request parameters. + * @param viewName the name of the view to resolve + * @param model the model to be passed to the view + * @param locale the current locale + * @param request current HTTP servlet request + * @return the View object, or null if none found + * @throws Exception if the view cannot be resolved + * (typically in case of problems creating an actual View object) + * @see ViewResolver#resolveViewName + */ + protected View resolveViewName(String viewName, Map model, Locale locale, HttpServletRequest request) + throws Exception { + + for (Iterator it = this.viewResolvers.iterator(); it.hasNext();) { + ViewResolver viewResolver = (ViewResolver) it.next(); + View view = viewResolver.resolveViewName(viewName, locale); + if (view != null) { + return view; + } + } + return null; + } + + /** + * Trigger afterCompletion callbacks on the mapped HandlerInterceptors. + * Will just invoke afterCompletion for all interceptors whose preHandle + * invocation has successfully completed and returned true. + * @param mappedHandler the mapped HandlerExecutionChain + * @param interceptorIndex index of last interceptor that successfully completed + * @param ex Exception thrown on handler execution, or null if none + * @see HandlerInterceptor#afterCompletion + */ + private void triggerAfterCompletion( + HandlerExecutionChain mappedHandler, int interceptorIndex, + HttpServletRequest request, HttpServletResponse response, Exception ex) + throws Exception { + + // Apply afterCompletion methods of registered interceptors. + if (mappedHandler != null) { + HandlerInterceptor[] interceptors = mappedHandler.getInterceptors(); + if (interceptors != null) { + for (int i = interceptorIndex; i >= 0; i--) { + HandlerInterceptor interceptor = interceptors[i]; + try { + interceptor.afterCompletion(request, response, mappedHandler.getHandler(), ex); + } + catch (Throwable ex2) { + logger.error("HandlerInterceptor.afterCompletion threw exception", ex2); + } + } + } + } + } + + /** + * Restore the request attributes after an include. + * @param request current HTTP request + * @param attributesSnapshot the snapshot of the request attributes + * before the include + */ + private void restoreAttributesAfterInclude(HttpServletRequest request, Map attributesSnapshot) { + logger.debug("Restoring snapshot of request attributes after include"); + + // Need to copy into separate Collection here, to avoid side effects + // on the Enumeration when removing attributes. + Set attrsToCheck = new HashSet(); + Enumeration attrNames = request.getAttributeNames(); + while (attrNames.hasMoreElements()) { + String attrName = (String) attrNames.nextElement(); + if (this.cleanupAfterInclude || attrName.startsWith("org.springframework.web.servlet")) { + attrsToCheck.add(attrName); + } + } + + // Iterate over the attributes to check, restoring the original value + // or removing the attribute, respectively, if appropriate. + for (Iterator it = attrsToCheck.iterator(); it.hasNext();) { + String attrName = (String) it.next(); + Object attrValue = attributesSnapshot.get(attrName); + if (attrValue != null) { + if (logger.isDebugEnabled()) { + logger.debug("Restoring original value of attribute [" + attrName + "] after include"); + } + request.setAttribute(attrName, attrValue); + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Removing attribute [" + attrName + "] after include"); + } + request.removeAttribute(attrName); + } + } + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/DispatcherServlet.properties b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/DispatcherServlet.properties new file mode 100644 index 00000000000..977a04a99bf --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/DispatcherServlet.properties @@ -0,0 +1,19 @@ +# Default implementation classes for DispatcherServlet's strategy interfaces. +# Used as fallback when no matching beans are found in the DispatcherServlet context. +# Not meant to be customized by application developers. + +org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver + +org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver + +org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\ + org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping + +org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\ + org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\ + org.springframework.web.servlet.mvc.throwaway.ThrowawayControllerHandlerAdapter,\ + org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter + +org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator + +org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/FrameworkServlet.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/FrameworkServlet.java new file mode 100644 index 00000000000..dc323d87f2d --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/FrameworkServlet.java @@ -0,0 +1,647 @@ +/* + * Copyright 2002-2008 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.web.servlet; + +import java.io.IOException; +import java.security.Principal; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextException; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.context.event.SourceFilteringListener; +import org.springframework.web.context.ConfigurableWebApplicationContext; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.ServletRequestHandledEvent; +import org.springframework.web.context.support.WebApplicationContextUtils; +import org.springframework.web.context.support.XmlWebApplicationContext; +import org.springframework.web.util.NestedServletException; +import org.springframework.web.util.WebUtils; + +/** + * Base servlet for Spring's web framework. Provides integration with + * a Spring application context, in a JavaBean-based overall solution. + * + *

This class offers the following functionality: + *

+ * + *

Subclasses must implement {@link #doService} to handle requests. Because this extends + * {@link HttpServletBean} rather than HttpServlet directly, bean properties are + * automatically mapped onto it. Subclasses can override {@link #initFrameworkServlet()} + * for custom initialization. + * + *

Detects a "contextClass" parameter at the servlet init-param level, + * falling back to the default context class, + * {@link org.springframework.web.context.support.XmlWebApplicationContext}, + * if not found. Note that, with the default FrameworkServlet, + * a custom context class needs to implement the + * {@link org.springframework.web.context.ConfigurableWebApplicationContext} SPI. + * + *

Passes a "contextConfigLocation" servlet init-param to the context instance, + * parsing it into potentially multiple file paths which can be separated by any + * number of commas and spaces, like "test-servlet.xml, myServlet.xml". + * If not explicitly specified, the context implementation is supposed to build a + * default location from the namespace of the servlet. + * + *

Note: In case of multiple config locations, later bean definitions will + * override ones defined in earlier loaded files, at least when using Spring's + * default ApplicationContext implementation. This can be leveraged to + * deliberately override certain bean definitions via an extra XML file. + * + *

The default namespace is "'servlet-name'-servlet", e.g. "test-servlet" for a + * servlet-name "test" (leading to a "/WEB-INF/test-servlet.xml" default location + * with XmlWebApplicationContext). The namespace can also be set explicitly via + * the "namespace" servlet init-param. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Sam Brannen + * @see #doService + * @see #setContextClass + * @see #setContextConfigLocation + * @see #setNamespace + */ +public abstract class FrameworkServlet extends HttpServletBean implements ApplicationListener { + + /** + * Suffix for WebApplicationContext namespaces. If a servlet of this class is + * given the name "test" in a context, the namespace used by the servlet will + * resolve to "test-servlet". + */ + public static final String DEFAULT_NAMESPACE_SUFFIX = "-servlet"; + + /** + * Default context class for FrameworkServlet. + * @see org.springframework.web.context.support.XmlWebApplicationContext + */ + public static final Class DEFAULT_CONTEXT_CLASS = XmlWebApplicationContext.class; + + /** + * Prefix for the ServletContext attribute for the WebApplicationContext. + * The completion is the servlet name. + */ + public static final String SERVLET_CONTEXT_PREFIX = FrameworkServlet.class.getName() + ".CONTEXT."; + + + /** ServletContext attribute to find the WebApplicationContext in */ + private String contextAttribute; + + /** WebApplicationContext implementation class to create */ + private Class contextClass = DEFAULT_CONTEXT_CLASS; + + /** Namespace for this servlet */ + private String namespace; + + /** Explicit context config location */ + private String contextConfigLocation; + + /** Should we publish the context as a ServletContext attribute? */ + private boolean publishContext = true; + + /** Should we publish a ServletRequestHandledEvent at the end of each request? */ + private boolean publishEvents = true; + + /** Should we dispatch an HTTP OPTIONS request to {@link #doService}? */ + private boolean dispatchOptionsRequest = false; + + /** Should we dispatch an HTTP TRACE request to {@link #doService}? */ + private boolean dispatchTraceRequest = false; + + /** WebApplicationContext for this servlet */ + private WebApplicationContext webApplicationContext; + + /** Flag used to detect whether onRefresh has already been called */ + private boolean refreshEventReceived = false; + + + /** + * Set the name of the ServletContext attribute which should be used to retrieve the + * {@link WebApplicationContext} that this servlet is supposed to use. + */ + public void setContextAttribute(String contextAttribute) { + this.contextAttribute = contextAttribute; + } + + /** + * Return the name of the ServletContext attribute which should be used to retrieve the + * {@link WebApplicationContext} that this servlet is supposed to use. + */ + public String getContextAttribute() { + return this.contextAttribute; + } + + /** + * Set a custom context class. This class must be of type + * {@link org.springframework.web.context.WebApplicationContext}. + *

When using the default FrameworkServlet implementation, + * the context class must also implement the + * {@link org.springframework.web.context.ConfigurableWebApplicationContext} + * interface. + * @see #createWebApplicationContext + */ + public void setContextClass(Class contextClass) { + this.contextClass = contextClass; + } + + /** + * Return the custom context class. + */ + public Class getContextClass() { + return this.contextClass; + } + + /** + * Set a custom namespace for this servlet, + * to be used for building a default context config location. + */ + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + /** + * Return the namespace for this servlet, falling back to default scheme if + * no custom namespace was set: e.g. "test-servlet" for a servlet named "test". + */ + public String getNamespace() { + return (this.namespace != null ? this.namespace : getServletName() + DEFAULT_NAMESPACE_SUFFIX); + } + + /** + * Set the context config location explicitly, instead of relying on the default + * location built from the namespace. This location string can consist of + * multiple locations separated by any number of commas and spaces. + */ + public void setContextConfigLocation(String contextConfigLocation) { + this.contextConfigLocation = contextConfigLocation; + } + + /** + * Return the explicit context config location, if any. + */ + public String getContextConfigLocation() { + return this.contextConfigLocation; + } + + /** + * Set whether to publish this servlet's context as a ServletContext attribute, + * available to all objects in the web container. Default is "true". + *

This is especially handy during testing, although it is debatable whether + * it's good practice to let other application objects access the context this way. + */ + public void setPublishContext(boolean publishContext) { + this.publishContext = publishContext; + } + + /** + * Set whether this servlet should publish a ServletRequestHandledEvent at the end + * of each request. Default is "true"; can be turned off for a slight performance + * improvement, provided that no ApplicationListeners rely on such events. + * @see org.springframework.web.context.support.ServletRequestHandledEvent + */ + public void setPublishEvents(boolean publishEvents) { + this.publishEvents = publishEvents; + } + + /** + * Set whether this servlet should dispatch an HTTP OPTIONS request to + * the {@link #doService} method. + *

Default is "false", applying {@link javax.servlet.http.HttpServlet}'s + * default behavior (i.e. enumerating all standard HTTP request methods + * as a response to the OPTIONS request). + *

Turn this flag on if you prefer OPTIONS requests to go through the + * regular dispatching chain, just like other HTTP requests. This usually + * means that your controllers will receive those requests; make sure + * that those endpoints are actually able to handle an OPTIONS request. + *

Note that HttpServlet's default OPTIONS processing will be applied + * in any case. Your controllers are simply available to override the + * default headers and optionally generate a response body. + */ + public void setDispatchOptionsRequest(boolean dispatchOptionsRequest) { + this.dispatchOptionsRequest = dispatchOptionsRequest; + } + + /** + * Set whether this servlet should dispatch an HTTP TRACE request to + * the {@link #doService} method. + *

Default is "false", applying {@link javax.servlet.http.HttpServlet}'s + * default behavior (i.e. reflecting the message received back to the client). + *

Turn this flag on if you prefer TRACE requests to go through the + * regular dispatching chain, just like other HTTP requests. This usually + * means that your controllers will receive those requests; make sure + * that those endpoints are actually able to handle a TRACE request. + *

Note that HttpServlet's default TRACE processing will be applied + * in any case. Your controllers are simply available to override the + * default headers and the default body, calling response.reset() + * if necessary. + */ + public void setDispatchTraceRequest(boolean dispatchTraceRequest) { + this.dispatchTraceRequest = dispatchTraceRequest; + } + + + /** + * Overridden method of {@link HttpServletBean}, invoked after any bean properties + * have been set. Creates this servlet's WebApplicationContext. + */ + protected final void initServletBean() throws ServletException, BeansException { + getServletContext().log("Initializing Spring FrameworkServlet '" + getServletName() + "'"); + if (this.logger.isInfoEnabled()) { + this.logger.info("FrameworkServlet '" + getServletName() + "': initialization started"); + } + long startTime = System.currentTimeMillis(); + + try { + this.webApplicationContext = initWebApplicationContext(); + initFrameworkServlet(); + } + catch (ServletException ex) { + this.logger.error("Context initialization failed", ex); + throw ex; + } + catch (BeansException ex) { + this.logger.error("Context initialization failed", ex); + throw ex; + } + + if (this.logger.isInfoEnabled()) { + long elapsedTime = System.currentTimeMillis() - startTime; + this.logger.info("FrameworkServlet '" + getServletName() + "': initialization completed in " + + elapsedTime + " ms"); + } + } + + /** + * Initialize and publish the WebApplicationContext for this servlet. + *

Delegates to {@link #createWebApplicationContext} for actual creation + * of the context. Can be overridden in subclasses. + * @return the WebApplicationContext instance + * @throws BeansException if the context couldn't be initialized + * @see #setContextClass + * @see #setContextConfigLocation + */ + protected WebApplicationContext initWebApplicationContext() throws BeansException { + WebApplicationContext wac = findWebApplicationContext(); + if (wac == null) { + // No fixed context defined for this servlet - create a local one. + WebApplicationContext parent = + WebApplicationContextUtils.getWebApplicationContext(getServletContext()); + wac = createWebApplicationContext(parent); + } + + if (!this.refreshEventReceived) { + // Apparently not a ConfigurableApplicationContext with refresh support: + // triggering initial onRefresh manually here. + onRefresh(wac); + } + + if (this.publishContext) { + // Publish the context as a servlet context attribute. + String attrName = getServletContextAttributeName(); + getServletContext().setAttribute(attrName, wac); + if (this.logger.isDebugEnabled()) { + this.logger.debug("Published WebApplicationContext of servlet '" + getServletName() + + "' as ServletContext attribute with name [" + attrName + "]"); + } + } + + return wac; + } + + /** + * Retrieve a WebApplicationContext from the ServletContext + * attribute with the {@link #setContextAttribute configured name}. The + * WebApplicationContext must have already been loaded and stored in the + * ServletContext before this servlet gets initialized (or invoked). + *

Subclasses may override this method to provide a different + * WebApplicationContext retrieval strategy. + * @return the WebApplicationContext for this servlet, or null if not found + * @see #getContextAttribute() + */ + protected WebApplicationContext findWebApplicationContext() { + String attrName = getContextAttribute(); + if (attrName == null) { + return null; + } + WebApplicationContext wac = + WebApplicationContextUtils.getWebApplicationContext(getServletContext(), attrName); + if (wac == null) { + throw new IllegalStateException("No WebApplicationContext found: initializer not registered?"); + } + return wac; + } + + /** + * Instantiate the WebApplicationContext for this servlet, either a default + * {@link org.springframework.web.context.support.XmlWebApplicationContext} + * or a {@link #setContextClass custom context class}, if set. + *

This implementation expects custom contexts to implement the + * {@link org.springframework.web.context.ConfigurableWebApplicationContext} + * interface. Can be overridden in subclasses. + *

Do not forget to register this servlet instance as application listener on the + * created context (for triggering its {@link #onRefresh callback}, and to call + * {@link org.springframework.context.ConfigurableApplicationContext#refresh()} + * before returning the context instance. + * @param parent the parent ApplicationContext to use, or null if none + * @return the WebApplicationContext for this servlet + * @throws BeansException if the context couldn't be initialized + * @see org.springframework.web.context.support.XmlWebApplicationContext + */ + protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) + throws BeansException { + + if (this.logger.isDebugEnabled()) { + this.logger.debug("Servlet with name '" + getServletName() + + "' will try to create custom WebApplicationContext context of class '" + + getContextClass().getName() + "'" + ", using parent context [" + parent + "]"); + } + if (!ConfigurableWebApplicationContext.class.isAssignableFrom(getContextClass())) { + throw new ApplicationContextException( + "Fatal initialization error in servlet with name '" + getServletName() + + "': custom WebApplicationContext class [" + getContextClass().getName() + + "] is not of type ConfigurableWebApplicationContext"); + } + + ConfigurableWebApplicationContext wac = + (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(getContextClass()); + wac.setParent(parent); + wac.setServletContext(getServletContext()); + wac.setServletConfig(getServletConfig()); + wac.setNamespace(getNamespace()); + wac.setConfigLocation(getContextConfigLocation()); + wac.addApplicationListener(new SourceFilteringListener(wac, this)); + + postProcessWebApplicationContext(wac); + wac.refresh(); + + return wac; + } + + /** + * Post-process the given WebApplicationContext before it is refreshed + * and activated as context for this servlet. + *

The default implementation is empty. refresh() will + * be called automatically after this method returns. + * @param wac the configured WebApplicationContext (not refreshed yet) + * @see #createWebApplicationContext + * @see ConfigurableWebApplicationContext#refresh() + */ + protected void postProcessWebApplicationContext(ConfigurableWebApplicationContext wac) { + } + + /** + * Return the ServletContext attribute name for this servlet's WebApplicationContext. + *

The default implementation returns + * SERVLET_CONTEXT_PREFIX + servlet name. + * @see #SERVLET_CONTEXT_PREFIX + * @see #getServletName + */ + public String getServletContextAttributeName() { + return SERVLET_CONTEXT_PREFIX + getServletName(); + } + + /** + * Return this servlet's WebApplicationContext. + */ + public final WebApplicationContext getWebApplicationContext() { + return this.webApplicationContext; + } + + + /** + * This method will be invoked after any bean properties have been set and + * the WebApplicationContext has been loaded. The default implementation is empty; + * subclasses may override this method to perform any initialization they require. + * @throws ServletException in case of an initialization exception + * @throws BeansException if thrown by ApplicationContext methods + */ + protected void initFrameworkServlet() throws ServletException, BeansException { + } + + /** + * Refresh this servlet's application context, as well as the + * dependent state of the servlet. + * @throws BeansException in case of errors + * @see #getWebApplicationContext() + * @see org.springframework.context.ConfigurableApplicationContext#refresh() + */ + public void refresh() throws BeansException { + WebApplicationContext wac = getWebApplicationContext(); + if (!(wac instanceof ConfigurableApplicationContext)) { + throw new IllegalStateException("WebApplicationContext does not support refresh: " + wac); + } + ((ConfigurableApplicationContext) wac).refresh(); + } + + /** + * ApplicationListener endpoint that receives events from this servlet's + * WebApplicationContext. + *

The default implementation calls {@link #onRefresh} in case of a + * {@link org.springframework.context.event.ContextRefreshedEvent}, + * triggering a refresh of this servlet's context-dependent state. + * @param event the incoming ApplicationContext event + */ + public void onApplicationEvent(ApplicationEvent event) { + if (event instanceof ContextRefreshedEvent) { + this.refreshEventReceived = true; + onRefresh(((ContextRefreshedEvent) event).getApplicationContext()); + } + } + + /** + * Template method which can be overridden to add servlet-specific refresh work. + * Called after successful context refresh. + *

This implementation is empty. + * @param context the current WebApplicationContext + * @throws BeansException in case of errors + * @see #refresh() + */ + protected void onRefresh(ApplicationContext context) throws BeansException { + // For subclasses: do nothing by default. + } + + + /** + * Delegate GET requests to processRequest/doService. + *

Will also be invoked by HttpServlet's default implementation of doHead, + * with a NoBodyResponse that just captures the content length. + * @see #doService + * @see #doHead + */ + protected final void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + processRequest(request, response); + } + + /** + * Delegate POST requests to {@link #processRequest}. + * @see #doService + */ + protected final void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + processRequest(request, response); + } + + /** + * Delegate PUT requests to {@link #processRequest}. + * @see #doService + */ + protected final void doPut(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + processRequest(request, response); + } + + /** + * Delegate DELETE requests to {@link #processRequest}. + * @see #doService + */ + protected final void doDelete(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + processRequest(request, response); + } + + /** + * Delegate OPTIONS requests to {@link #processRequest}, if desired. + *

Applies HttpServlet's standard OPTIONS processing first. + * @see #doService + */ + protected void doOptions(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + super.doOptions(request, response); + if (this.dispatchOptionsRequest) { + processRequest(request, response); + } + } + + /** + * Delegate TRACE requests to {@link #processRequest}, if desired. + *

Applies HttpServlet's standard TRACE processing first. + * @see #doService + */ + protected void doTrace(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + super.doTrace(request, response); + if (this.dispatchTraceRequest) { + processRequest(request, response); + } + } + + + /** + * Process this request, publishing an event regardless of the outcome. + *

The actual event handling is performed by the abstract + * {@link #doService} template method. + */ + protected final void processRequest(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + long startTime = System.currentTimeMillis(); + Throwable failureCause = null; + + try { + doService(request, response); + } + catch (ServletException ex) { + failureCause = ex; + throw ex; + } + catch (IOException ex) { + failureCause = ex; + throw ex; + } + catch (Throwable ex) { + failureCause = ex; + throw new NestedServletException("Request processing failed", ex); + } + + finally { + if (failureCause != null) { + this.logger.debug("Could not complete request", failureCause); + } + else { + this.logger.debug("Successfully completed request"); + } + if (this.publishEvents) { + // Whether or not we succeeded, publish an event. + long processingTime = System.currentTimeMillis() - startTime; + this.webApplicationContext.publishEvent( + new ServletRequestHandledEvent(this, + request.getRequestURI(), request.getRemoteAddr(), + request.getMethod(), getServletConfig().getServletName(), + WebUtils.getSessionId(request), getUsernameForRequest(request), + processingTime, failureCause)); + } + } + } + + /** + * Determine the username for the given request. + *

The default implementation takes the name of the UserPrincipal, if any. + * Can be overridden in subclasses. + * @param request current HTTP request + * @return the username, or null if none found + * @see javax.servlet.http.HttpServletRequest#getUserPrincipal() + */ + protected String getUsernameForRequest(HttpServletRequest request) { + Principal userPrincipal = request.getUserPrincipal(); + return (userPrincipal != null ? userPrincipal.getName() : null); + } + + /** + * Subclasses must implement this method to do the work of request handling, + * receiving a centralized callback for GET, POST, PUT and DELETE. + *

The contract is essentially the same as that for the commonly overridden + * doGet or doPost methods of HttpServlet. + *

This class intercepts calls to ensure that exception handling and + * event publication takes place. + * @param request current HTTP request + * @param response current HTTP response + * @throws Exception in case of any kind of processing failure + * @see javax.servlet.http.HttpServlet#doGet + * @see javax.servlet.http.HttpServlet#doPost + */ + protected abstract void doService(HttpServletRequest request, HttpServletResponse response) + throws Exception; + + + /** + * Close the WebApplicationContext of this servlet. + * @see org.springframework.context.ConfigurableApplicationContext#close() + */ + public void destroy() { + getServletContext().log("Destroying Spring FrameworkServlet '" + getServletName() + "'"); + if (this.webApplicationContext instanceof ConfigurableApplicationContext) { + ((ConfigurableApplicationContext) this.webApplicationContext).close(); + } + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/HandlerAdapter.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/HandlerAdapter.java new file mode 100644 index 00000000000..fe61230af39 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/HandlerAdapter.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2006 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.web.servlet; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * MVC framework SPI interface, allowing parameterization of core MVC workflow. + * + *

Interface that must be implemented for each handler type to handle a request. + * This interface is used to allow the DispatcherServlet to be indefinitely + * extensible. The DispatcherServlet accesses all installed handlers through this + * interface, meaning that it does not contain code specific to any handler type. + * + *

Note that a handler can be of type Object. This is to enable handlers from + * other frameworks to be integrated with this framework without custom coding. + * + *

This interface is not intended for application developers. It is available + * to handlers who want to develop their own web workflow. + * + *

Note: Implementations can implement the Ordered interface to be able to + * specify a sorting order and thus a priority for getting applied by + * DispatcherServlet. Non-Ordered instances get treated as lowest priority. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter + * @see org.springframework.web.servlet.mvc.throwaway.ThrowawayControllerHandlerAdapter + * @see org.springframework.web.servlet.handler.SimpleServletHandlerAdapter + */ +public interface HandlerAdapter { + + /** + * Given a handler instance, return whether or not this HandlerAdapter can + * support it. Typical HandlerAdapters will base the decision on the handler + * type. HandlerAdapters will usually only support one handler type each. + *

A typical implementation: + *

+ * return (handler instanceof MyHandler); + * + * @param handler handler object to check + * @return whether or not this object can use the given handler + */ + boolean supports(Object handler); + + /** + * Use the given handler to handle this request. + * The workflow that is required may vary widely. + * @param request current HTTP request + * @param response current HTTP response + * @param handler handler to use. This object must have previously been passed + * to the supports method of this interface, which must have + * returned true. + * @throws Exception in case of errors + * @return ModelAndView object with the name of the view and the required + * model data, or null if the request has been handled directly + */ + ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception; + + /** + * Same contract as for HttpServlet's getLastModified method. + * Can simply return -1 if there's no support in the handler class. + * @param request current HTTP request + * @param handler handler to use + * @return the lastModified value for the given handler + * @see javax.servlet.http.HttpServlet#getLastModified + * @see org.springframework.web.servlet.mvc.LastModified#getLastModified + */ + long getLastModified(HttpServletRequest request, Object handler); + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/HandlerExceptionResolver.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/HandlerExceptionResolver.java new file mode 100644 index 00000000000..7b843335c76 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/HandlerExceptionResolver.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2007 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.web.servlet; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Interface to be implemented by objects than can resolve exceptions thrown + * during handler mapping or execution, in the typical case to error views. + * Implementors are typically registered as beans in the application context. + * + *

Error views are analogous to the error page JSPs, but can be used with + * any kind of exception including any checked exception, with potentially + * fine-granular mappings for specific handlers. + * + * @author Juergen Hoeller + * @since 22.11.2003 + */ +public interface HandlerExceptionResolver { + + /** + * Try to resolve the given exception that got thrown during on handler execution, + * returning a ModelAndView that represents a specific error page if appropriate. + * @param request current HTTP request + * @param response current HTTP response + * @param handler the executed handler, or null if none chosen at the + * time of the exception (for example, if multipart resolution failed) + * @param ex the exception that got thrown during handler execution + * @return a corresponding ModelAndView to forward to, or null for default processing + */ + ModelAndView resolveException( + HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex); + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/HandlerExecutionChain.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/HandlerExecutionChain.java new file mode 100644 index 00000000000..98f3cdbfb25 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/HandlerExecutionChain.java @@ -0,0 +1,122 @@ +/* + * Copyright 2002-2007 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.web.servlet; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.util.CollectionUtils; + +/** + * Handler execution chain, consisting of handler object and any handler interceptors. + * Returned by HandlerMapping's {@link HandlerMapping#getHandler} method. + * + * @author Juergen Hoeller + * @since 20.06.2003 + * @see HandlerInterceptor + */ +public class HandlerExecutionChain { + + private final Object handler; + + private HandlerInterceptor[] interceptors; + + private List interceptorList; + + + /** + * Create a new HandlerExecutionChain. + * @param handler the handler object to execute + */ + public HandlerExecutionChain(Object handler) { + this(handler, null); + } + + /** + * Create a new HandlerExecutionChain. + * @param handler the handler object to execute + * @param interceptors the array of interceptors to apply + * (in the given order) before the handler itself executes + */ + public HandlerExecutionChain(Object handler, HandlerInterceptor[] interceptors) { + if (handler instanceof HandlerExecutionChain) { + HandlerExecutionChain originalChain = (HandlerExecutionChain) handler; + this.handler = originalChain.getHandler(); + this.interceptorList = new ArrayList(); + CollectionUtils.mergeArrayIntoCollection(originalChain.getInterceptors(), this.interceptorList); + CollectionUtils.mergeArrayIntoCollection(interceptors, this.interceptorList); + } + else { + this.handler = handler; + this.interceptors = interceptors; + } + } + + + /** + * Return the handler object to execute. + * @return the handler object + */ + public Object getHandler() { + return this.handler; + } + + public void addInterceptor(HandlerInterceptor interceptor) { + initInterceptorList(); + this.interceptorList.add(interceptor); + } + + public void addInterceptors(HandlerInterceptor[] interceptors) { + if (interceptors != null) { + initInterceptorList(); + for (int i = 0; i < interceptors.length; i++) { + this.interceptorList.add(interceptors[i]); + } + } + } + + private void initInterceptorList() { + if (this.interceptorList == null) { + this.interceptorList = new ArrayList(); + } + if (this.interceptors != null) { + for (int i = 0; i < this.interceptors.length; i++) { + this.interceptorList.add(this.interceptors[i]); + } + this.interceptors = null; + } + } + + /** + * Return the array of interceptors to apply (in the given order). + * @return the array of HandlerInterceptors instances (may be null) + */ + public HandlerInterceptor[] getInterceptors() { + if (this.interceptors == null && this.interceptorList != null) { + this.interceptors = (HandlerInterceptor[]) + this.interceptorList.toArray(new HandlerInterceptor[this.interceptorList.size()]); + } + return this.interceptors; + } + + /** + * Delegates to the handler's toString(). + */ + public String toString() { + return String.valueOf(handler); + } +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/HandlerInterceptor.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/HandlerInterceptor.java new file mode 100644 index 00000000000..1c74e9bb73a --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/HandlerInterceptor.java @@ -0,0 +1,120 @@ +/* + * Copyright 2002-2006 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.web.servlet; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Workflow interface that allows for customized handler execution chains. + * Applications can register any number of existing or custom interceptors + * for certain groups of handlers, to add common preprocessing behavior + * without needing to modify each handler implementation. + * + *

A HandlerInterceptor gets called before the appropriate HandlerAdapter + * triggers the execution of the handler itself. This mechanism can be used + * for a large field of preprocessing aspects, e.g. for authorization checks, + * or common handler behavior like locale or theme changes. Its main purpose + * is to allow for factoring out repetitive handler code. + * + *

Typically an interceptor chain is defined per HandlerMapping bean, + * sharing its granularity. To be able to apply a certain interceptor chain + * to a group of handlers, one needs to map the desired handlers via one + * HandlerMapping bean. The interceptors themselves are defined as beans + * in the application context, referenced by the mapping bean definition + * via its "interceptors" property (in XML: a <list> of <ref>). + * + *

HandlerInterceptor is basically similar to a Servlet 2.3 Filter, but in + * contrast to the latter it just allows custom pre-processing with the option + * of prohibiting the execution of the handler itself, and custom post-processing. + * Filters are more powerful, for example they allow for exchanging the request + * and response objects that are handed down the chain. Note that a filter + * gets configured in web.xml, a HandlerInterceptor in the application context. + * + *

As a basic guideline, fine-grained handler-related preprocessing tasks are + * candidates for HandlerInterceptor implementations, especially factored-out + * common handler code and authorization checks. On the other hand, a Filter + * is well-suited for request content and view content handling, like multipart + * forms and GZIP compression. This typically shows when one needs to map the + * filter to certain content types (e.g. images), or to all requests. + * + * @author Juergen Hoeller + * @since 20.06.2003 + * @see HandlerExecutionChain#getInterceptors + * @see org.springframework.web.servlet.handler.HandlerInterceptorAdapter + * @see org.springframework.web.servlet.handler.AbstractHandlerMapping#setInterceptors + * @see org.springframework.web.servlet.handler.UserRoleAuthorizationInterceptor + * @see org.springframework.web.servlet.i18n.LocaleChangeInterceptor + * @see org.springframework.web.servlet.theme.ThemeChangeInterceptor + * @see javax.servlet.Filter + */ +public interface HandlerInterceptor { + + /** + * Intercept the execution of a handler. Called after HandlerMapping determined + * an appropriate handler object, but before HandlerAdapter invokes the handler. + *

DispatcherServlet processes a handler in an execution chain, consisting + * of any number of interceptors, with the handler itself at the end. + * With this method, each interceptor can decide to abort the execution chain, + * typically sending a HTTP error or writing a custom response. + * @param request current HTTP request + * @param response current HTTP response + * @param handler chosen handler to execute, for type and/or instance evaluation + * @return true if the execution chain should proceed with the + * next interceptor or the handler itself. Else, DispatcherServlet assumes + * that this interceptor has already dealt with the response itself. + * @throws Exception in case of errors + */ + boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception; + + /** + * Intercept the execution of a handler. Called after HandlerAdapter actually + * invoked the handler, but before the DispatcherServlet renders the view. + * Can expose additional model objects to the view via the given ModelAndView. + *

DispatcherServlet processes a handler in an execution chain, consisting + * of any number of interceptors, with the handler itself at the end. + * With this method, each interceptor can post-process an execution, + * getting applied in inverse order of the execution chain. + * @param request current HTTP request + * @param response current HTTP response + * @param handler chosen handler to execute, for type and/or instance examination + * @param modelAndView the ModelAndView that the handler returned + * (can also be null) + * @throws Exception in case of errors + */ + void postHandle( + HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) + throws Exception; + + /** + * Callback after completion of request processing, that is, after rendering + * the view. Will be called on any outcome of handler execution, thus allows + * for proper resource cleanup. + *

Note: Will only be called if this interceptor's preHandle + * method has successfully completed and returned true! + * @param request current HTTP request + * @param response current HTTP response + * @param handler chosen handler to execute, for type and/or instance examination + * @param ex exception thrown on handler execution, if any + * @throws Exception in case of errors + */ + void afterCompletion( + HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) + throws Exception; + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/HandlerMapping.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/HandlerMapping.java new file mode 100644 index 00000000000..6458beea6d0 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/HandlerMapping.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2007 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.web.servlet; + +import javax.servlet.http.HttpServletRequest; + +/** + * Interface to be implemented by objects that define a mapping between + * requests and handler objects. + * + *

This class can be implemented by application developers, although this is not + * necessary, as {@link org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping} + * and {@link org.springframework.web.servlet.handler.SimpleUrlHandlerMapping} + * are included in the framework. The former is the default if no + * HandlerMapping bean is registered in the application context. + * + *

HandlerMapping implementations can support mapped interceptors but do not + * have to. A handler will always be wrapped in a {@link HandlerExecutionChain} + * instance, optionally accompanied by some {@link HandlerInterceptor} instances. + * The DispatcherServlet will first call each HandlerInterceptor's + * preHandle method in the given order, finally invoking the handler + * itself if all preHandle methods have returned true. + * + *

The ability to parameterize this mapping is a powerful and unusual + * capability of this MVC framework. For example, it is possible to write + * a custom mapping based on session state, cookie state or many other + * variables. No other MVC framework seems to be equally flexible. + * + *

Note: Implementations can implement the {@link org.springframework.core.Ordered} + * interface to be able to specify a sorting order and thus a priority for getting + * applied by DispatcherServlet. Non-Ordered instances get treated as lowest priority. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see org.springframework.core.Ordered + * @see org.springframework.web.servlet.handler.AbstractHandlerMapping + * @see org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping + * @see org.springframework.web.servlet.handler.SimpleUrlHandlerMapping + */ +public interface HandlerMapping { + + /** + * Name of the {@link HttpServletRequest} attribute that contains the path + * within the handler mapping, in case of a pattern match, or the full + * relevant URI (typically within the DispatcherServlet's mapping) else. + *

Note: This attribute is not required to be supported by all + * HandlerMapping implementations. URL-based HandlerMappings will + * typically support it, but handlers should not necessarily expect + * this request attribute to be present in all scenarios. + */ + String PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE = HandlerMapping.class.getName() + ".pathWithinHandlerMapping"; + + + /** + * Return a handler and any interceptors for this request. The choice may be made + * on request URL, session state, or any factor the implementing class chooses. + *

The returned HandlerExecutionChain contains a handler Object, rather than + * even a tag interface, so that handlers are not constrained in any way. + * For example, a HandlerAdapter could be written to allow another framework's + * handler objects to be used. + *

Returns null if no match was found. This is not an error. + * The DispatcherServlet will query all registered HandlerMapping beans to find + * a match, and only decide there is an error if none can find a handler. + * @param request current HTTP request + * @return a HandlerExecutionChain instance containing handler object and + * any interceptors, or null if no mapping found + * @throws Exception if there is an internal error + */ + HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception; + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/HttpServletBean.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/HttpServletBean.java new file mode 100644 index 00000000000..3d650438023 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/HttpServletBean.java @@ -0,0 +1,213 @@ +/* + * Copyright 2002-2008 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.web.servlet; + +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.BeansException; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.PropertyAccessorFactory; +import org.springframework.beans.PropertyValue; +import org.springframework.beans.PropertyValues; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceEditor; +import org.springframework.core.io.ResourceLoader; +import org.springframework.util.StringUtils; +import org.springframework.web.context.support.ServletContextResourceLoader; + +/** + * Simple extension of {@link javax.servlet.http.HttpServlet} which treats + * its config parameters (init-param entries within the + * servlet tag in web.xml) as bean properties. + * + *

A handy superclass for any type of servlet. Type conversion of config + * parameters is automatic, with the corresponding setter method getting + * invoked with the converted value. It is also possible for subclasses to + * specify required properties. Parameters without matching bean property + * setter will simply be ignored. + * + *

This servlet leaves request handling to subclasses, inheriting the default + * behavior of HttpServlet (doGet, doPost, etc). + * + *

This generic servlet base class has no dependency on the Spring + * {@link org.springframework.context.ApplicationContext} concept. Simple + * servlets usually don't load their own context but rather access service + * beans from the Spring root application context, accessible via the + * filter's {@link #getServletContext() ServletContext} (see + * {@link org.springframework.web.context.support.WebApplicationContextUtils}). + * + *

The {@link FrameworkServlet} class is a more specific servlet base + * class which loads its own application context. FrameworkServlet serves + * as direct base class of Spring's full-fledged {@link DispatcherServlet}. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see #addRequiredProperty + * @see #initServletBean + * @see #doGet + * @see #doPost + */ +public abstract class HttpServletBean extends HttpServlet { + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + /** + * Set of required properties (Strings) that must be supplied as + * config parameters to this servlet. + */ + private final Set requiredProperties = new HashSet(); + + + /** + * Subclasses can invoke this method to specify that this property + * (which must match a JavaBean property they expose) is mandatory, + * and must be supplied as a config parameter. This should be called + * from the constructor of a subclass. + *

This method is only relevant in case of traditional initialization + * driven by a ServletConfig instance. + * @param property name of the required property + */ + protected final void addRequiredProperty(String property) { + this.requiredProperties.add(property); + } + + /** + * Map config parameters onto bean properties of this servlet, and + * invoke subclass initialization. + * @throws ServletException if bean properties are invalid (or required + * properties are missing), or if subclass initialization fails. + */ + public final void init() throws ServletException { + if (logger.isDebugEnabled()) { + logger.debug("Initializing servlet '" + getServletName() + "'"); + } + + // Set bean properties from init parameters. + try { + PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties); + BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this); + ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext()); + bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader)); + initBeanWrapper(bw); + bw.setPropertyValues(pvs, true); + } + catch (BeansException ex) { + logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex); + throw ex; + } + + // Let subclasses do whatever initialization they like. + initServletBean(); + + if (logger.isDebugEnabled()) { + logger.debug("Servlet '" + getServletName() + "' configured successfully"); + } + } + + /** + * Initialize the BeanWrapper for this HttpServletBean, + * possibly with custom editors. + *

This default implementation is empty. + * @param bw the BeanWrapper to initialize + * @throws BeansException if thrown by BeanWrapper methods + * @see org.springframework.beans.BeanWrapper#registerCustomEditor + */ + protected void initBeanWrapper(BeanWrapper bw) throws BeansException { + } + + + /** + * Overridden method that simply returns null when no + * ServletConfig set yet. + * @see #getServletConfig() + */ + public final String getServletName() { + return (getServletConfig() != null ? getServletConfig().getServletName() : null); + } + + /** + * Overridden method that simply returns null when no + * ServletConfig set yet. + * @see #getServletConfig() + */ + public final ServletContext getServletContext() { + return (getServletConfig() != null ? getServletConfig().getServletContext() : null); + } + + + /** + * Subclasses may override this to perform custom initialization. + * All bean properties of this servlet will have been set before this + * method is invoked. + *

This default implementation is empty. + * @throws ServletException if subclass initialization fails + */ + protected void initServletBean() throws ServletException { + } + + + /** + * PropertyValues implementation created from ServletConfig init parameters. + */ + private static class ServletConfigPropertyValues extends MutablePropertyValues { + + /** + * Create new ServletConfigPropertyValues. + * @param config ServletConfig we'll use to take PropertyValues from + * @param requiredProperties set of property names we need, where + * we can't accept default values + * @throws ServletException if any required properties are missing + */ + public ServletConfigPropertyValues(ServletConfig config, Set requiredProperties) + throws ServletException { + + Set missingProps = (requiredProperties != null && !requiredProperties.isEmpty()) ? + new HashSet(requiredProperties) : null; + + Enumeration en = config.getInitParameterNames(); + while (en.hasMoreElements()) { + String property = (String) en.nextElement(); + Object value = config.getInitParameter(property); + addPropertyValue(new PropertyValue(property, value)); + if (missingProps != null) { + missingProps.remove(property); + } + } + + // Fail if we are still missing properties. + if (missingProps != null && missingProps.size() > 0) { + throw new ServletException( + "Initialization from ServletConfig for servlet '" + config.getServletName() + + "' failed; the following required properties were missing: " + + StringUtils.collectionToDelimitedString(missingProps, ", ")); + } + } + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/LocaleResolver.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/LocaleResolver.java new file mode 100644 index 00000000000..7a953e6e889 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/LocaleResolver.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2006 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.web.servlet; + +import java.util.Locale; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Interface for web-based locale resolution strategies that allows for + * both locale resolution via the request and locale modification via + * request and response. + * + *

This interface allows for implementations based on request, session, + * cookies, etc. The default implementation is AcceptHeaderLocaleResolver, + * simply using the request's locale provided by the respective HTTP header. + * + *

Use RequestContext.getLocale() to retrieve the current locale + * in controllers or views, independent of the actual resolution strategy. + * + * @author Juergen Hoeller + * @since 27.02.2003 + * @see org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver + * @see org.springframework.web.servlet.support.RequestContext#getLocale + */ +public interface LocaleResolver { + + /** + * Resolve the current locale via the given request. + * Should return a default locale as fallback in any case. + * @param request the request to resolve the locale for + * @return the current locale (never null) + */ + Locale resolveLocale(HttpServletRequest request); + + /** + * Set the current locale to the given one. + * @param request the request to be used for locale modification + * @param response the response to be used for locale modification + * @param locale the new locale, or null to clear the locale + * @throws UnsupportedOperationException if the LocaleResolver implementation + * does not support dynamic changing of the theme + */ + void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale); + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/ModelAndView.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/ModelAndView.java new file mode 100644 index 00000000000..fa0a5745bff --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/ModelAndView.java @@ -0,0 +1,303 @@ +/* + * Copyright 2002-2006 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.web.servlet; + +import java.util.Map; + +import org.springframework.ui.ModelMap; + +/** + * Holder for both Model and View in the web MVC framework. + * Note that these are entirely distinct. This class merely holds + * both to make it possible for a controller to return both model + * and view in a single return value. + * + *

Represents a model and view returned by a handler, to be resolved + * by a DispatcherServlet. The view can take the form of a String + * view name which will need to be resolved by a ViewResolver object; + * alternatively a View object can be specified directly. The model + * is a Map, allowing the use of multiple objects keyed by name. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Rob Harrop + * @see DispatcherServlet + * @see ViewResolver + * @see HandlerAdapter#handle + * @see org.springframework.web.servlet.mvc.Controller#handleRequest + */ +public class ModelAndView { + + /** View instance or view name String */ + private Object view; + + /** Model Map */ + private ModelMap model; + + /** + * Indicates whether or not this instance has been cleared with a call to {@link #clear()}. + */ + private boolean cleared; + + + /** + * Default constructor for bean-style usage: populating bean + * properties instead of passing in constructor arguments. + * @see #setView(View) + * @see #setViewName(String) + */ + public ModelAndView() { + } + + /** + * Convenient constructor when there is no model data to expose. + * Can also be used in conjunction with addObject. + * @param viewName name of the View to render, to be resolved + * by the DispatcherServlet's ViewResolver + * @see #addObject + */ + public ModelAndView(String viewName) { + this.view = viewName; + } + + /** + * Convenient constructor when there is no model data to expose. + * Can also be used in conjunction with addObject. + * @param view View object to render + * @see #addObject + */ + public ModelAndView(View view) { + this.view = view; + } + + /** + * Creates new ModelAndView given a view name and a model. + * @param viewName name of the View to render, to be resolved + * by the DispatcherServlet's ViewResolver + * @param model Map of model names (Strings) to model objects + * (Objects). Model entries may not be null, but the + * model Map may be null if there is no model data. + */ + public ModelAndView(String viewName, Map model) { + this.view = viewName; + if (model != null) { + getModelMap().addAllAttributes(model); + } + } + + /** + * Creates new ModelAndView given a View object and a model. + * Note: the supplied model data is copied into the internal + * storage of this class. You should not consider to modify the supplied + * Map after supplying it to this class + * @param view View object to render + * @param model Map of model names (Strings) to model objects + * (Objects). Model entries may not be null, but the + * model Map may be null if there is no model data. + */ + public ModelAndView(View view, Map model) { + this.view = view; + if (model != null) { + getModelMap().addAllAttributes(model); + } + } + + /** + * Convenient constructor to take a single model object. + * @param viewName name of the View to render, to be resolved + * by the DispatcherServlet's ViewResolver + * @param modelName name of the single entry in the model + * @param modelObject the single model object + */ + public ModelAndView(String viewName, String modelName, Object modelObject) { + this.view = viewName; + addObject(modelName, modelObject); + } + + /** + * Convenient constructor to take a single model object. + * @param view View object to render + * @param modelName name of the single entry in the model + * @param modelObject the single model object + */ + public ModelAndView(View view, String modelName, Object modelObject) { + this.view = view; + addObject(modelName, modelObject); + } + + + /** + * Set a view name for this ModelAndView, to be resolved by the + * DispatcherServlet via a ViewResolver. Will override any + * pre-existing view name or View. + */ + public void setViewName(String viewName) { + this.view = viewName; + } + + /** + * Return the view name to be resolved by the DispatcherServlet + * via a ViewResolver, or null if we are using a View object. + */ + public String getViewName() { + return (this.view instanceof String ? (String) this.view : null); + } + + /** + * Set a View object for this ModelAndView. Will override any + * pre-existing view name or View. + */ + public void setView(View view) { + this.view = view; + } + + /** + * Return the View object, or null if we are using a view name + * to be resolved by the DispatcherServlet via a ViewResolver. + */ + public View getView() { + return (this.view instanceof View ? (View) this.view : null); + } + + /** + * Indicate whether or not this ModelAndView has a view, either + * as a view name or as a direct {@link View} instance. + */ + public boolean hasView() { + return (this.view != null); + } + + /** + * Return whether we use a view reference, i.e. true + * if the view has been specified via a name to be resolved by the + * DispatcherServlet via a ViewResolver. + */ + public boolean isReference() { + return (this.view instanceof String); + } + + /** + * Return the model map. May return null. + * Called by DispatcherServlet for evaluation of the model. + */ + protected Map getModelInternal() { + return this.model; + } + + /** + * Return the underlying ModelMap instance (never null). + */ + public ModelMap getModelMap() { + if (this.model == null) { + this.model = new ModelMap(); + } + return this.model; + } + + /** + * Return the model map. Never returns null. + * To be called by application code for modifying the model. + */ + public Map getModel() { + return getModelMap(); + } + + + /** + * Add an attribute to the model. + * @param attributeName name of the object to add to the model + * @param attributeValue object to add to the model (never null) + * @see ModelMap#addAttribute(String, Object) + * @see #getModelMap() + */ + public ModelAndView addObject(String attributeName, Object attributeValue) { + getModelMap().addAttribute(attributeName, attributeValue); + return this; + } + + /** + * Add an attribute to the model using parameter name generation. + * @param attributeValue the object to add to the model (never null) + * @see ModelMap#addAttribute(Object) + * @see #getModelMap() + */ + public ModelAndView addObject(Object attributeValue) { + getModelMap().addAttribute(attributeValue); + return this; + } + + /** + * Add all attributes contained in the provided Map to the model. + * @param modelMap a Map of attributeName -> attributeValue pairs + * @see ModelMap#addAllAttributes(Map) + * @see #getModelMap() + */ + public ModelAndView addAllObjects(Map modelMap) { + getModelMap().addAllAttributes(modelMap); + return this; + } + + + /** + * Clear the state of this ModelAndView object. + * The object will be empty afterwards. + *

Can be used to suppress rendering of a given ModelAndView object + * in the postHandle method of a HandlerInterceptor. + * @see #isEmpty() + * @see HandlerInterceptor#postHandle + */ + public void clear() { + this.view = null; + this.model = null; + this.cleared = true; + } + + /** + * Return whether this ModelAndView object is empty + * i.e. whether it does not hold any view and does not contain a model. + */ + public boolean isEmpty() { + return (this.view == null && this.model == null); + } + + /** + * Return whether this ModelAndView object is empty as a result of a call to {@link #clear} + * i.e. whether it does not hold any view and does not contain a model. + * Returns false if any additional state was added to the instance + * after the call to {@link #clear}. + * @see #clear() + */ + public boolean wasCleared() { + return (this.cleared && isEmpty()); + } + + + /** + * Return diagnostic information about this model and view. + */ + public String toString() { + StringBuffer buf = new StringBuffer("ModelAndView: "); + if (isReference()) { + buf.append("reference to view with name '").append(this.view).append("'"); + } + else { + buf.append("materialized View is [").append(this.view).append(']'); + } + buf.append("; model is ").append(this.model); + return buf.toString(); + } +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/ModelAndViewDefiningException.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/ModelAndViewDefiningException.java new file mode 100644 index 00000000000..36ef10866fc --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/ModelAndViewDefiningException.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2005 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.web.servlet; + +import javax.servlet.ServletException; + +import org.springframework.util.Assert; + +/** + * Exception to be thrown on error conditions that should forward + * to a specific view with a specific model. + * + *

Can be thrown at any time during handler processing. + * This includes any template methods of pre-built controllers. + * For example, a form controller might abort to a specific error page + * if certain parameters do not allow to proceed with the normal workflow. + * + * @author Juergen Hoeller + * @since 22.11.2003 + */ +public class ModelAndViewDefiningException extends ServletException { + + private ModelAndView modelAndView; + + + /** + * Create new ModelAndViewDefiningException with the given ModelAndView, + * typically representing a specific error page. + * @param modelAndView ModelAndView with view to forward to and model to expose + */ + public ModelAndViewDefiningException(ModelAndView modelAndView) { + Assert.notNull(modelAndView, "ModelAndView must not be null in ModelAndViewDefiningException"); + this.modelAndView = modelAndView; + } + + /** + * Return the ModelAndView that this exception contains for forwarding to. + */ + public ModelAndView getModelAndView() { + return modelAndView; + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/RequestToViewNameTranslator.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/RequestToViewNameTranslator.java new file mode 100644 index 00000000000..dc534599616 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/RequestToViewNameTranslator.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2007 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.web.servlet; + +import javax.servlet.http.HttpServletRequest; + +/** + * Strategy interface for translating an incoming + * {@link javax.servlet.http.HttpServletRequest} into a + * logical view name when no view name is explicitly supplied. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + */ +public interface RequestToViewNameTranslator { + + /** + * Translate the given {@link HttpServletRequest} into a view name. + * @param request the incoming {@link HttpServletRequest} providing + * the context from which a view name is to be resolved + * @return the view name (or null if no default found) + * @throws Exception if view name translation fails + */ + String getViewName(HttpServletRequest request) throws Exception; + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/ResourceServlet.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/ResourceServlet.java new file mode 100644 index 00000000000..54e5a957a83 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/ResourceServlet.java @@ -0,0 +1,342 @@ +/* + * Copyright 2002-2008 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.web.servlet; + +import java.io.IOException; + +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.util.AntPathMatcher; +import org.springframework.util.PathMatcher; +import org.springframework.util.StringUtils; +import org.springframework.web.context.support.ServletContextResource; + +/** + * Simple servlet that can expose an internal resource, including a + * default URL if the specified resource is not found. An alternative, + * for example, to trying and catching exceptions when using JSP include. + * + *

A further usage of this servlet is the ability to apply last-modified + * timestamps to quasi-static resources (typically JSPs). This can happen + * as bridge to parameter-specified resources, or as proxy for a specific + * target resource (or a list of specific target resources to combine). + * + *

A typical usage would map a URL like "/ResourceServlet" onto an instance + * of this servlet, and use the "JSP include" action to include this URL, + * with the "resource" parameter indicating the actual target path in the WAR. + * + *

The defaultUrl property can be set to the internal + * resource path of a default URL, to be rendered when the target resource + * is not found or not specified in the first place. + * + *

The "resource" parameter and the defaultUrl property can + * also specify a list of target resources to combine. Those resources will be + * included one by one to build the response. If last-modified determination + * is active, the newest timestamp among those files will be used. + * + *

The allowedResources property can be set to a URL + * pattern of resources that should be available via this servlet. + * If not set, any target resource can be requested, including resources + * in the WEB-INF directory! + * + *

If using this servlet for direct access rather than via includes, + * the contentType property should be specified to apply a + * proper content type. Note that a content type header in the target JSP will + * be ignored when including the resource via a RequestDispatcher include. + * + *

To apply last-modified timestamps for the target resource, set the + * applyLastModified property to true. This servlet will then + * return the file timestamp of the target resource as last-modified value, + * falling back to the startup time of this servlet if not retrievable. + * + *

Note that applying the last-modified timestamp in the above fashion + * just makes sense if the target resource does not generate content that + * depends on the HttpSession or cookies; it is just allowed to evaluate + * request parameters. + * + *

A typical case for such last-modified usage is a JSP that just makes + * minimal usage of basic means like includes or message resolution to + * build quasi-static content. Regenerating such content on every request + * is unnecessary; it can be cached as long as the file hasn't changed. + * + *

Note that this servlet will apply the last-modified timestamp if you + * tell it to do so: It's your decision whether the content of the target + * resource can be cached in such a fashion. Typical use cases are helper + * resources that are not fronted by a controller, like JavaScript files + * that are generated by a JSP (without depending on the HttpSession). + * + * @author Juergen Hoeller + * @author Rod Johnson + * @see #setDefaultUrl + * @see #setAllowedResources + * @see #setApplyLastModified + */ +public class ResourceServlet extends HttpServletBean { + + /** + * Any number of these characters are considered delimiters + * between multiple resource paths in a single String value. + */ + public static final String RESOURCE_URL_DELIMITERS = ",; \t\n"; + + /** + * Name of the parameter that must contain the actual resource path. + */ + public static final String RESOURCE_PARAM_NAME = "resource"; + + + private String defaultUrl; + + private String allowedResources; + + private String contentType; + + private boolean applyLastModified = false; + + private PathMatcher pathMatcher; + + private long startupTime; + + + /** + * Set the URL within the current web application from which to + * include content if the requested path isn't found, or if none + * is specified in the first place. + *

If specifying multiple URLs, they will be included one by one + * to build the response. If last-modified determination is active, + * the newest timestamp among those files will be used. + * @see #setApplyLastModified + */ + public void setDefaultUrl(String defaultUrl) { + this.defaultUrl = defaultUrl; + } + + /** + * Set allowed resources as URL pattern, e.g. "/WEB-INF/res/*.jsp", + * The parameter can be any Ant-style pattern parsable by AntPathMatcher. + * @see org.springframework.util.AntPathMatcher + */ + public void setAllowedResources(String allowedResources) { + this.allowedResources = allowedResources; + } + + /** + * Set the content type of the target resource (typically a JSP). + * Default is none, which is appropriate when including resources. + *

For directly accessing resources, for example to leverage this + * servlet's last-modified support, specify a content type here. + * Note that a content type header in the target JSP will be ignored + * when including the resource via a RequestDispatcher include. + */ + public void setContentType(String contentType) { + this.contentType = contentType; + } + + /** + * Set whether to apply the file timestamp of the target resource + * as last-modified value. Default is "false". + *

This is mainly intended for JSP targets that don't generate + * session-specific or database-driven content: Such files can be + * cached by the browser as long as the last-modified timestamp + * of the JSP file doesn't change. + *

This will only work correctly with expanded WAR files that + * allow access to the file timestamps. Else, the startup time + * of this servlet is returned. + */ + public void setApplyLastModified(boolean applyLastModified) { + this.applyLastModified = applyLastModified; + } + + + /** + * Remember the startup time, using no last-modified time before it. + */ + protected void initServletBean() { + this.pathMatcher = getPathMatcher(); + this.startupTime = System.currentTimeMillis(); + } + + /** + * Return a PathMatcher to use for matching the "allowedResources" URL pattern. + * Default is AntPathMatcher. + * @see #setAllowedResources + * @see org.springframework.util.AntPathMatcher + */ + protected PathMatcher getPathMatcher() { + return new AntPathMatcher(); + } + + + /** + * Determine the URL of the target resource and include it. + * @see #determineResourceUrl + */ + protected final void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + // determine URL of resource to include + String resourceUrl = determineResourceUrl(request); + + if (resourceUrl != null) { + try { + doInclude(request, response, resourceUrl); + } + catch (ServletException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Failed to include content of resource [" + resourceUrl + "]", ex); + } + // Try including default URL if appropriate. + if (!includeDefaultUrl(request, response)) { + throw ex; + } + } + catch (IOException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Failed to include content of resource [" + resourceUrl + "]", ex); + } + // Try including default URL if appropriate. + if (!includeDefaultUrl(request, response)) { + throw ex; + } + } + } + + // no resource URL specified -> try to include default URL. + else if (!includeDefaultUrl(request, response)) { + throw new ServletException("No target resource URL found for request"); + } + } + + /** + * Determine the URL of the target resource of this request. + *

Default implementation returns the value of the "resource" parameter. + * Can be overridden in subclasses. + * @param request current HTTP request + * @return the URL of the target resource, or null if none found + * @see #RESOURCE_PARAM_NAME + */ + protected String determineResourceUrl(HttpServletRequest request) { + return request.getParameter(RESOURCE_PARAM_NAME); + } + + /** + * Include the specified default URL, if appropriate. + * @param request current HTTP request + * @param response current HTTP response + * @return whether a default URL was included + * @throws ServletException if thrown by the RequestDispatcher + * @throws IOException if thrown by the RequestDispatcher + */ + private boolean includeDefaultUrl(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + if (this.defaultUrl == null) { + return false; + } + doInclude(request, response, this.defaultUrl); + return true; + } + + /** + * Include the specified resource via the RequestDispatcher. + * @param request current HTTP request + * @param response current HTTP response + * @param resourceUrl the URL of the target resource + * @throws ServletException if thrown by the RequestDispatcher + * @throws IOException if thrown by the RequestDispatcher + */ + private void doInclude(HttpServletRequest request, HttpServletResponse response, String resourceUrl) + throws ServletException, IOException { + + if (this.contentType != null) { + response.setContentType(this.contentType); + } + String[] resourceUrls = + StringUtils.tokenizeToStringArray(resourceUrl, RESOURCE_URL_DELIMITERS); + for (int i = 0; i < resourceUrls.length; i++) { + // check whether URL matches allowed resources + if (this.allowedResources != null && !this.pathMatcher.match(this.allowedResources, resourceUrls[i])) { + throw new ServletException("Resource [" + resourceUrls[i] + + "] does not match allowed pattern [" + this.allowedResources + "]"); + } + if (logger.isDebugEnabled()) { + logger.debug("Including resource [" + resourceUrls[i] + "]"); + } + RequestDispatcher rd = request.getRequestDispatcher(resourceUrls[i]); + rd.include(request, response); + } + } + + /** + * Return the last-modified timestamp of the file that corresponds + * to the target resource URL (i.e. typically the request ".jsp" file). + * Will simply return -1 if "applyLastModified" is false (the default). + *

Returns no last-modified date before the startup time of this servlet, + * to allow for message resolution etc that influences JSP contents, + * assuming that those background resources might have changed on restart. + *

Returns the startup time of this servlet if the file that corresponds + * to the target resource URL coudln't be resolved (for example, because + * the WAR is not expanded). + * @see #determineResourceUrl + * @see #getFileTimestamp + */ + protected final long getLastModified(HttpServletRequest request) { + if (this.applyLastModified) { + String resourceUrl = determineResourceUrl(request); + if (resourceUrl == null) { + resourceUrl = this.defaultUrl; + } + if (resourceUrl != null) { + String[] resourceUrls = StringUtils.tokenizeToStringArray(resourceUrl, RESOURCE_URL_DELIMITERS); + long latestTimestamp = -1; + for (int i = 0; i < resourceUrls.length; i++) { + long timestamp = getFileTimestamp(resourceUrls[i]); + if (timestamp > latestTimestamp) { + latestTimestamp = timestamp; + } + } + return (latestTimestamp > this.startupTime ? latestTimestamp : this.startupTime); + } + } + return -1; + } + + /** + * Return the file timestamp for the given resource. + * @param resourceUrl the URL of the resource + * @return the file timestamp in milliseconds, or -1 if not determinable + */ + protected long getFileTimestamp(String resourceUrl) { + ServletContextResource resource = new ServletContextResource(getServletContext(), resourceUrl); + try { + long lastModifiedTime = resource.lastModified(); + if (logger.isDebugEnabled()) { + logger.debug("Last-modified timestamp of " + resource + " is " + lastModifiedTime); + } + return lastModifiedTime; + } + catch (IOException ex) { + logger.warn("Couldn't retrieve last-modified timestamp of [" + resource + + "] - using ResourceServlet startup time"); + return -1; + } + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/ThemeResolver.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/ThemeResolver.java new file mode 100644 index 00000000000..c1463fc9e94 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/ThemeResolver.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2005 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.web.servlet; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Interface for web-based theme resolution strategies that allows for + * both theme resolution via the request and theme modification via + * request and response. + * + *

This interface allows for implementations based on session, + * cookies, etc. The default implementation is FixedThemeResolver, + * simply using a configured default theme. + * + *

Note that this resolver is only responsible for determining the + * current theme name. The Theme instance for the resolved theme name + * gets looked up by DispatcherServlet via the respective ThemeSource, + * i.e. the current WebApplicationContext. + * + *

Use RequestContext.getTheme() to retrieve the current theme in + * controllers or views, independent of the actual resolution strategy. + * + * @author Jean-Pierre Pawlak + * @author Juergen Hoeller + * @since 17.06.2003 + * @see org.springframework.web.servlet.theme.FixedThemeResolver + * @see org.springframework.ui.context.Theme + * @see org.springframework.ui.context.ThemeSource + * @see org.springframework.web.servlet.support.RequestContext#getTheme + */ +public interface ThemeResolver { + + /** + * Resolve the current theme name via the given request. + * Should return a default theme as fallback in any case. + * @param request request to be used for resolution + * @return the current theme name + */ + String resolveThemeName(HttpServletRequest request); + + /** + * Set the current theme name to the given one. + * @param request request to be used for theme name modification + * @param response response to be used for theme name modification + * @param themeName the new theme name + * @throws UnsupportedOperationException if the ThemeResolver implementation + * does not support dynamic changing of the theme + */ + void setThemeName(HttpServletRequest request, HttpServletResponse response, String themeName); + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/View.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/View.java new file mode 100644 index 00000000000..aa7550deeb8 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/View.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2005 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.web.servlet; + +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * MVC View for a web interaction. Implementations are responsible for rendering + * content, and exposing the model. A single view exposes multiple model attributes. + * + *

This class and the MVC approach associated with it is discussed in Chapter 12 of + * Expert One-On-One J2EE Design and Development + * by Rod Johnson (Wrox, 2002). + * + *

View implementations may differ widely. An obvious implementation would be + * JSP-based. Other implementations might be XSLT-based, or use an HTML generation library. + * This interface is designed to avoid restricting the range of possible implementations. + * + *

Views should be beans. They are likely to be instantiated as beans by a ViewResolver. + * As this interface is stateless, view implementations should be thread-safe. + * + * @author Rod Johnson + * @see org.springframework.web.servlet.view.AbstractView + * @see org.springframework.web.servlet.view.InternalResourceView + */ +public interface View { + + /** + * Return the content type of the view, if predetermined. + *

Can be used to check the content type upfront, + * before the actual rendering process. + * @return the content type String (optionally including a character set), + * or null if not predetermined. + */ + String getContentType(); + + /** + * Render the view given the specified model. + *

The first step will be preparing the request: In the JSP case, + * this would mean setting model objects as request attributes. + * The second step will be the actual rendering of the view, + * for example including the JSP via a RequestDispatcher. + * @param model Map with name Strings as keys and corresponding model + * objects as values (Map can also be null in case of empty model) + * @param request current HTTP request + * @param response HTTP response we are building + * @throws Exception if rendering failed + */ + void render(Map model, HttpServletRequest request, HttpServletResponse response) throws Exception; + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/ViewRendererServlet.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/ViewRendererServlet.java new file mode 100644 index 00000000000..0c59f328a6d --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/ViewRendererServlet.java @@ -0,0 +1,114 @@ +/* + * Copyright 2002-2006 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.web.servlet; + +import java.io.IOException; +import java.util.Map; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.web.util.NestedServletException; + +/** + * ViewRendererServlet is a bridge servlet, mainly for the Portlet MVC support. + * + *

For usage with Portlets, this Servlet is necessary to force the portlet container + * to convert the PortletRequest to a ServletRequest, which it has to do when + * including a resource via the PortletRequestDispatcher. This allows for reuse + * of the entire Servlet-based View support even in a Portlet environment. + * + *

The actual mapping of the bridge servlet is configurable in the DispatcherPortlet, + * via a "viewRendererUrl" property. The default is "/WEB-INF/servlet/view", which is + * just available for internal resource dispatching. + * + * @author William G. Thompson, Jr. + * @author John A. Lewis + * @author Juergen Hoeller + * @since 2.0 + */ +public class ViewRendererServlet extends HttpServlet { + + /** + * Request attribute to hold current web application context. + * Otherwise only the global web app context is obtainable by tags etc. + * @see org.springframework.web.servlet.support.RequestContextUtils#getWebApplicationContext + */ + public static final String WEB_APPLICATION_CONTEXT_ATTRIBUTE = DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE; + + /** Name of request attribute that holds the View object */ + public static final String VIEW_ATTRIBUTE = ViewRendererServlet.class.getName() + ".VIEW"; + + /** Name of request attribute that holds the model Map */ + public static final String MODEL_ATTRIBUTE = ViewRendererServlet.class.getName() + ".MODEL"; + + + protected final void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + processRequest(request, response); + } + + protected final void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + processRequest(request, response); + } + + /** + * Process this request, handling exceptions. + * The actually event handling is performed by the abstract + * renderView() template method. + * @see #renderView + */ + protected final void processRequest(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + try { + renderView(request, response); + } + catch (ServletException ex) { + throw ex; + } + catch (IOException ex) { + throw ex; + } + catch (Exception ex) { + throw new NestedServletException("View rendering failed", ex); + } + } + + /** + * Retrieve the View instance and model Map to render + * and trigger actual rendering. + * @param request current HTTP request + * @param response current HTTP response + * @throws Exception in case of any kind of processing failure + * @see org.springframework.web.servlet.View#render + */ + protected void renderView(HttpServletRequest request, HttpServletResponse response) throws Exception { + View view = (View) request.getAttribute(VIEW_ATTRIBUTE); + if (view == null) { + throw new ServletException("Could not complete render request: View is null"); + } + Map model = (Map) request.getAttribute(MODEL_ATTRIBUTE); + view.render(model, request, response); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/ViewResolver.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/ViewResolver.java new file mode 100644 index 00000000000..d9b4ff0be73 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/ViewResolver.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2005 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.web.servlet; + +import java.util.Locale; + +/** + * Interface to be implemented by objects that can resolve views by name. + * + *

View state doesn't change during the running of the application, + * so implementations are free to cache views. + * + *

Implementations are encouraged to support internationalization, + * i.e. localized view resolution. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see org.springframework.web.servlet.view.InternalResourceViewResolver + * @see org.springframework.web.servlet.view.ResourceBundleViewResolver + * @see org.springframework.web.servlet.view.XmlViewResolver + */ +public interface ViewResolver { + + /** + * Resolve the given view by name. + *

Note: To allow for ViewResolver chaining, a ViewResolver should + * return null if a view with the given name is not defined in it. + * However, this is not required: Some ViewResolvers will always attempt + * to build View objects with the given name, unable to return null + * (rather throwing an exception when View creation failed). + * @param viewName name of the view to resolve + * @param locale Locale in which to resolve the view. + * ViewResolvers that support internationalization should respect this. + * @return the View object, or null if not found + * (optional, to allow for ViewResolver chaining) + * @throws Exception if the view cannot be resolved + * (typically in case of problems creating an actual View object) + */ + View resolveViewName(String viewName, Locale locale) throws Exception; + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/AbstractDetectingUrlHandlerMapping.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/AbstractDetectingUrlHandlerMapping.java new file mode 100644 index 00000000000..10b99b7edbe --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/AbstractDetectingUrlHandlerMapping.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002-2008 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.web.servlet.handler; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.context.ApplicationContextException; +import org.springframework.util.ObjectUtils; + +/** + * Abstract implementation of the {@link org.springframework.web.servlet.HandlerMapping} + * interface, detecting URL mappings for handler beans through introspection of all + * defined beans in the application context. + * + * @author Juergen Hoeller + * @since 2.5 + * @see #determineUrlsForHandler + */ +public abstract class AbstractDetectingUrlHandlerMapping extends AbstractUrlHandlerMapping { + + private boolean detectHandlersInAncestorContexts = false; + + + /** + * Set whether to detect handler beans in ancestor ApplicationContexts. + *

Default is "false": Only handler beans in the current ApplicationContext + * will be detected, i.e. only in the context that this HandlerMapping itself + * is defined in (typically the current DispatcherServlet's context). + *

Switch this flag on to detect handler beans in ancestor contexts + * (typically the Spring root WebApplicationContext) as well. + */ + public void setDetectHandlersInAncestorContexts(boolean detectHandlersInAncestorContexts) { + this.detectHandlersInAncestorContexts = detectHandlersInAncestorContexts; + } + + + /** + * Calls the {@link #detectHandlers()} method in addition to the + * superclass's initialization. + */ + public void initApplicationContext() throws ApplicationContextException { + super.initApplicationContext(); + detectHandlers(); + } + + /** + * Register all handlers found in the current ApplicationContext. + *

The actual URL determination for a handler is up to the concrete + * {@link #determineUrlsForHandler(String)} implementation. A bean for + * which no such URLs could be determined is simply not considered a handler. + * @throws org.springframework.beans.BeansException if the handler couldn't be registered + * @see #determineUrlsForHandler(String) + */ + protected void detectHandlers() throws BeansException { + if (logger.isDebugEnabled()) { + logger.debug("Looking for URL mappings in application context: " + getApplicationContext()); + } + String[] beanNames = (this.detectHandlersInAncestorContexts ? + BeanFactoryUtils.beanNamesForTypeIncludingAncestors(getApplicationContext(), Object.class) : + getApplicationContext().getBeanNamesForType(Object.class)); + + // Take any bean name that we can determine URLs for. + for (int i = 0; i < beanNames.length; i++) { + String beanName = beanNames[i]; + String[] urls = determineUrlsForHandler(beanName); + if (!ObjectUtils.isEmpty(urls)) { + // URL paths found: Let's consider it a handler. + registerHandler(urls, beanName); + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Rejected bean name '" + beanNames[i] + "': no URL paths identified"); + } + } + } + } + + + /** + * Determine the URLs for the given handler bean. + * @param beanName the name of the candidate bean + * @return the URLs determined for the bean, + * or null or an empty array if none + */ + protected abstract String[] determineUrlsForHandler(String beanName); + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java new file mode 100644 index 00000000000..f53d4da9c87 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java @@ -0,0 +1,238 @@ +/* + * Copyright 2002-2007 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.web.servlet.handler; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.beans.BeansException; +import org.springframework.core.Ordered; +import org.springframework.web.context.request.WebRequestInterceptor; +import org.springframework.web.context.support.WebApplicationObjectSupport; +import org.springframework.web.servlet.HandlerExecutionChain; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.HandlerMapping; + +/** + * Abstract base class for {@link org.springframework.web.servlet.HandlerMapping} + * implementations. Supports ordering, a default handler, and handler interceptors. + * + *

Note: This base class does not support exposure of the + * {@link #PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE}. Support for this attribute + * is up to concrete subclasses, typically based on request URL mappings. + * + * @author Juergen Hoeller + * @since 07.04.2003 + * @see #getHandlerInternal + * @see #setDefaultHandler + * @see #setInterceptors + * @see org.springframework.web.servlet.HandlerInterceptor + */ +public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport + implements HandlerMapping, Ordered { + + private int order = Integer.MAX_VALUE; // default: same as non-Ordered + + private Object defaultHandler; + + private final List interceptors = new ArrayList(); + + private HandlerInterceptor[] adaptedInterceptors; + + + /** + * Specify the order value for this HandlerMapping bean. + *

Default value is Integer.MAX_VALUE, meaning that it's non-ordered. + * @see org.springframework.core.Ordered#getOrder() + */ + public final void setOrder(int order) { + this.order = order; + } + + public final int getOrder() { + return this.order; + } + + /** + * Set the default handler for this handler mapping. + * This handler will be returned if no specific mapping was found. + *

Default is null, indicating no default handler. + */ + public void setDefaultHandler(Object defaultHandler) { + this.defaultHandler = defaultHandler; + } + + /** + * Return the default handler for this handler mapping, + * or null if none. + */ + public Object getDefaultHandler() { + return this.defaultHandler; + } + + /** + * Set the interceptors to apply for all handlers mapped by this handler mapping. + *

Supported interceptor types are HandlerInterceptor and WebRequestInterceptor. + * @param interceptors array of handler interceptors, or null if none + * @see #adaptInterceptor + * @see org.springframework.web.servlet.HandlerInterceptor + * @see org.springframework.web.context.request.WebRequestInterceptor + */ + public void setInterceptors(Object[] interceptors) { + this.interceptors.addAll(Arrays.asList(interceptors)); + } + + + /** + * Initializes the interceptors. + * @see #extendInterceptors(java.util.List) + * @see #initInterceptors() + */ + protected void initApplicationContext() throws BeansException { + extendInterceptors(this.interceptors); + initInterceptors(); + } + + /** + * Extension hook that subclasses can override to register additional interceptors, + * given the configured interceptors (see {@link #setInterceptors}). + *

Will be invoked before {@link #initInterceptors()} adapts the specified + * interceptors into {@link HandlerInterceptor} instances. + *

The default implementation is empty. + * @param interceptors the configured interceptor List (never null), + * allowing to add further interceptors before as well as after the existing + * interceptors + */ + protected void extendInterceptors(List interceptors) { + } + + /** + * Initialize the specified interceptors, adapting them where necessary. + * @see #setInterceptors + * @see #adaptInterceptor + */ + protected void initInterceptors() { + if (!this.interceptors.isEmpty()) { + this.adaptedInterceptors = new HandlerInterceptor[this.interceptors.size()]; + for (int i = 0; i < this.interceptors.size(); i++) { + Object interceptor = this.interceptors.get(i); + if (interceptor == null) { + throw new IllegalArgumentException("Entry number " + i + " in interceptors array is null"); + } + this.adaptedInterceptors[i] = adaptInterceptor(interceptor); + } + } + } + + /** + * Adapt the given interceptor object to the HandlerInterceptor interface. + *

Supported interceptor types are HandlerInterceptor and WebRequestInterceptor. + * Each given WebRequestInterceptor will be wrapped in a WebRequestHandlerInterceptorAdapter. + * Can be overridden in subclasses. + * @param interceptor the specified interceptor object + * @return the interceptor wrapped as HandlerInterceptor + * @see org.springframework.web.servlet.HandlerInterceptor + * @see org.springframework.web.context.request.WebRequestInterceptor + * @see WebRequestHandlerInterceptorAdapter + */ + protected HandlerInterceptor adaptInterceptor(Object interceptor) { + if (interceptor instanceof HandlerInterceptor) { + return (HandlerInterceptor) interceptor; + } + else if (interceptor instanceof WebRequestInterceptor) { + return new WebRequestHandlerInterceptorAdapter((WebRequestInterceptor) interceptor); + } + else { + throw new IllegalArgumentException("Interceptor type not supported: " + interceptor.getClass().getName()); + } + } + + /** + * Return the adapted interceptors as HandlerInterceptor array. + * @return the array of HandlerInterceptors, or null if none + */ + protected final HandlerInterceptor[] getAdaptedInterceptors() { + return this.adaptedInterceptors; + } + + + /** + * Look up a handler for the given request, falling back to the default + * handler if no specific one is found. + * @param request current HTTP request + * @return the corresponding handler instance, or the default handler + * @see #getHandlerInternal + */ + public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { + Object handler = getHandlerInternal(request); + if (handler == null) { + handler = getDefaultHandler(); + } + if (handler == null) { + return null; + } + // Bean name or resolved handler? + if (handler instanceof String) { + String handlerName = (String) handler; + handler = getApplicationContext().getBean(handlerName); + } + return getHandlerExecutionChain(handler, request); + } + + /** + * Look up a handler for the given request, returning null if no + * specific one is found. This method is called by {@link #getHandler}; + * a null return value will lead to the default handler, if one is set. + *

Note: This method may also return a pre-built {@link HandlerExecutionChain}, + * combining a handler object with dynamically determined interceptors. + * Statically specified interceptors will get merged into such an existing chain. + * @param request current HTTP request + * @return the corresponding handler instance, or null if none found + * @throws Exception if there is an internal error + */ + protected abstract Object getHandlerInternal(HttpServletRequest request) throws Exception; + + /** + * Build a HandlerExecutionChain for the given handler, including applicable interceptors. + *

The default implementation simply builds a standard HandlerExecutionChain with + * the given handler and this handler mapping's common interceptors. Subclasses may + * override this in order to extend/rearrange the list of interceptors. + *

NOTE: The passed-in handler object may be a raw handler or a pre-built + * HandlerExecutionChain. This method should handle those two cases explicitly, + * either building a new HandlerExecutionChain or extending the existing chain. + *

For simply adding an interceptor, consider calling super.getHandlerExecutionChain + * and invoking {@link HandlerExecutionChain#addInterceptor} on the returned chain object. + * @param handler the resolved handler instance (never null) + * @param request current HTTP request + * @return the HandlerExecutionChain (never null) + * @see #getAdaptedInterceptors() + */ + protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) { + if (handler instanceof HandlerExecutionChain) { + HandlerExecutionChain chain = (HandlerExecutionChain) handler; + chain.addInterceptors(getAdaptedInterceptors()); + return chain; + } + else { + return new HandlerExecutionChain(handler, getAdaptedInterceptors()); + } + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/AbstractUrlHandlerMapping.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/AbstractUrlHandlerMapping.java new file mode 100644 index 00000000000..0c6e0fda7e8 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/AbstractUrlHandlerMapping.java @@ -0,0 +1,362 @@ +/* + * Copyright 2002-2008 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.web.servlet.handler; + +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.BeansException; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.Assert; +import org.springframework.util.PathMatcher; +import org.springframework.web.servlet.HandlerExecutionChain; +import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.util.UrlPathHelper; + +/** + * Abstract base class for URL-mapped {@link org.springframework.web.servlet.HandlerMapping} + * implementations. Provides infrastructure for mapping handlers to URLs and configurable + * URL lookup. For information on the latter, see "alwaysUseFullPath" property. + * + *

Supports direct matches, e.g. a registered "/test" matches "/test", and + * various Ant-style pattern matches, e.g. a registered "/t*" pattern matches + * both "/test" and "/team", "/test/*" matches all paths in the "/test" directory, + * "/test/**" matches all paths below "/test". For details, see the + * {@link org.springframework.util.AntPathMatcher AntPathMatcher} javadoc. + * + *

Will search all path patterns to find the most exact match for the + * current request path. The most exact match is defined as the longest + * path pattern that matches the current request path. + * + * @author Juergen Hoeller + * @since 16.04.2003 + * @see #setAlwaysUseFullPath + * @see #setUrlDecode + * @see org.springframework.util.AntPathMatcher + */ +public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping { + + private UrlPathHelper urlPathHelper = new UrlPathHelper(); + + private PathMatcher pathMatcher = new AntPathMatcher(); + + private Object rootHandler; + + private boolean lazyInitHandlers = false; + + private final Map handlerMap = new LinkedHashMap(); + + + /** + * Set if URL lookup should always use the full path within the current servlet + * context. Else, the path within the current servlet mapping is used if applicable + * (that is, in the case of a ".../*" servlet mapping in web.xml). + *

Default is "false". + * @see org.springframework.web.util.UrlPathHelper#setAlwaysUseFullPath + */ + public void setAlwaysUseFullPath(boolean alwaysUseFullPath) { + this.urlPathHelper.setAlwaysUseFullPath(alwaysUseFullPath); + } + + /** + * Set if context path and request URI should be URL-decoded. Both are returned + * undecoded by the Servlet API, in contrast to the servlet path. + *

Uses either the request encoding or the default encoding according + * to the Servlet spec (ISO-8859-1). + * @see org.springframework.web.util.UrlPathHelper#setUrlDecode + */ + public void setUrlDecode(boolean urlDecode) { + this.urlPathHelper.setUrlDecode(urlDecode); + } + + /** + * Set the UrlPathHelper to use for resolution of lookup paths. + *

Use this to override the default UrlPathHelper with a custom subclass, + * or to share common UrlPathHelper settings across multiple HandlerMappings + * and MethodNameResolvers. + * @see org.springframework.web.servlet.mvc.multiaction.AbstractUrlMethodNameResolver#setUrlPathHelper + */ + public void setUrlPathHelper(UrlPathHelper urlPathHelper) { + Assert.notNull(urlPathHelper, "UrlPathHelper must not be null"); + this.urlPathHelper = urlPathHelper; + } + + /** + * Set the PathMatcher implementation to use for matching URL paths + * against registered URL patterns. Default is AntPathMatcher. + * @see org.springframework.util.AntPathMatcher + */ + public void setPathMatcher(PathMatcher pathMatcher) { + Assert.notNull(pathMatcher, "PathMatcher must not be null"); + this.pathMatcher = pathMatcher; + } + + /** + * Return the PathMatcher implementation to use for matching URL paths + * against registered URL patterns. + */ + public PathMatcher getPathMatcher() { + return this.pathMatcher; + } + + /** + * Set the root handler for this handler mapping, that is, + * the handler to be registered for the root path ("/"). + *

Default is null, indicating no root handler. + */ + public void setRootHandler(Object rootHandler) { + this.rootHandler = rootHandler; + } + + /** + * Return the root handler for this handler mapping (registered for "/"), + * or null if none. + */ + public Object getRootHandler() { + return this.rootHandler; + } + + /** + * Set whether to lazily initialize handlers. Only applicable to + * singleton handlers, as prototypes are always lazily initialized. + * Default is "false", as eager initialization allows for more efficiency + * through referencing the controller objects directly. + *

If you want to allow your controllers to be lazily initialized, + * make them "lazy-init" and set this flag to true. Just making them + * "lazy-init" will not work, as they are initialized through the + * references from the handler mapping in this case. + */ + public void setLazyInitHandlers(boolean lazyInitHandlers) { + this.lazyInitHandlers = lazyInitHandlers; + } + + + /** + * Look up a handler for the URL path of the given request. + * @param request current HTTP request + * @return the handler instance, or null if none found + */ + protected Object getHandlerInternal(HttpServletRequest request) throws Exception { + String lookupPath = this.urlPathHelper.getLookupPathForRequest(request); + Object handler = lookupHandler(lookupPath, request); + if (handler == null) { + // We need to care for the default handler directly, since we need to + // expose the PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE for it as well. + Object rawHandler = null; + if ("/".equals(lookupPath)) { + rawHandler = getRootHandler(); + } + if (rawHandler == null) { + rawHandler = getDefaultHandler(); + } + if (rawHandler != null) { + validateHandler(rawHandler, request); + handler = buildPathExposingHandler(rawHandler, lookupPath); + } + } + if (handler != null && logger.isDebugEnabled()) { + logger.debug("Mapping [" + lookupPath + "] to handler '" + handler + "'"); + } + else if (handler == null && logger.isTraceEnabled()) { + logger.trace("No handler mapping found for [" + lookupPath + "]"); + } + return handler; + } + + /** + * Look up a handler instance for the given URL path. + *

Supports direct matches, e.g. a registered "/test" matches "/test", + * and various Ant-style pattern matches, e.g. a registered "/t*" matches + * both "/test" and "/team". For details, see the AntPathMatcher class. + *

Looks for the most exact pattern, where most exact is defined as + * the longest path pattern. + * @param urlPath URL the bean is mapped to + * @param request current HTTP request (to expose the path within the mapping to) + * @return the associated handler instance, or null if not found + * @see #exposePathWithinMapping + * @see org.springframework.util.AntPathMatcher + */ + protected Object lookupHandler(String urlPath, HttpServletRequest request) throws Exception { + // Direct match? + Object handler = this.handlerMap.get(urlPath); + if (handler != null) { + validateHandler(handler, request); + return buildPathExposingHandler(handler, urlPath); + } + // Pattern match? + String bestPathMatch = null; + for (Iterator it = this.handlerMap.keySet().iterator(); it.hasNext();) { + String registeredPath = (String) it.next(); + if (getPathMatcher().match(registeredPath, urlPath) && + (bestPathMatch == null || bestPathMatch.length() < registeredPath.length())) { + bestPathMatch = registeredPath; + } + } + if (bestPathMatch != null) { + handler = this.handlerMap.get(bestPathMatch); + validateHandler(handler, request); + String pathWithinMapping = getPathMatcher().extractPathWithinPattern(bestPathMatch, urlPath); + return buildPathExposingHandler(handler, pathWithinMapping); + } + // No handler found... + return null; + } + + /** + * Validate the given handler against the current request. + *

The default implementation is empty. Can be overridden in subclasses, + * for example to enforce specific preconditions expressed in URL mappings. + * @param handler the handler object to validate + * @param request current HTTP request + * @throws Exception if validation failed + */ + protected void validateHandler(Object handler, HttpServletRequest request) throws Exception { + } + + /** + * Build a handler object for the given raw handler, exposing the actual + * handler as well as the {@link #PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE} + * before executing the handler. + *

The default implementation builds a {@link HandlerExecutionChain} + * with a special interceptor that exposes the path attribute. + * @param rawHandler the raw handler to expose + * @param pathWithinMapping the path to expose before executing the handler + * @return the final handler object + */ + protected Object buildPathExposingHandler(Object rawHandler, String pathWithinMapping) { + // Bean name or resolved handler? + if (rawHandler instanceof String) { + String handlerName = (String) rawHandler; + rawHandler = getApplicationContext().getBean(handlerName); + } + HandlerExecutionChain chain = new HandlerExecutionChain(rawHandler); + chain.addInterceptor(new PathExposingHandlerInterceptor(pathWithinMapping)); + return chain; + } + + /** + * Expose the path within the current mapping as request attribute. + * @param pathWithinMapping the path within the current mapping + * @param request the request to expose the path to + * @see #PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE + */ + protected void exposePathWithinMapping(String pathWithinMapping, HttpServletRequest request) { + request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, pathWithinMapping); + } + + + /** + * Register the specified handler for the given URL paths. + * @param urlPaths the URLs that the bean should be mapped to + * @param beanName the name of the handler bean + * @throws BeansException if the handler couldn't be registered + * @throws IllegalStateException if there is a conflicting handler registered + */ + protected void registerHandler(String[] urlPaths, String beanName) throws BeansException, IllegalStateException { + Assert.notNull(urlPaths, "URL path array must not be null"); + for (int j = 0; j < urlPaths.length; j++) { + registerHandler(urlPaths[j], beanName); + } + } + + /** + * Register the specified handler for the given URL path. + * @param urlPath the URL the bean should be mapped to + * @param handler the handler instance or handler bean name String + * (a bean name will automatically be resolved into the corresponding handler bean) + * @throws BeansException if the handler couldn't be registered + * @throws IllegalStateException if there is a conflicting handler registered + */ + protected void registerHandler(String urlPath, Object handler) throws BeansException, IllegalStateException { + Assert.notNull(urlPath, "URL path must not be null"); + Assert.notNull(handler, "Handler object must not be null"); + Object resolvedHandler = handler; + + // Eagerly resolve handler if referencing singleton via name. + if (!this.lazyInitHandlers && handler instanceof String) { + String handlerName = (String) handler; + if (getApplicationContext().isSingleton(handlerName)) { + resolvedHandler = getApplicationContext().getBean(handlerName); + } + } + + Object mappedHandler = this.handlerMap.get(urlPath); + if (mappedHandler != null) { + if (mappedHandler != resolvedHandler) { + throw new IllegalStateException( + "Cannot map handler [" + handler + "] to URL path [" + urlPath + + "]: There is already handler [" + resolvedHandler + "] mapped."); + } + } + else { + if (urlPath.equals("/")) { + if (logger.isDebugEnabled()) { + logger.debug("Root mapping to handler [" + resolvedHandler + "]"); + } + setRootHandler(resolvedHandler); + } + else if (urlPath.equals("/*")) { + if (logger.isDebugEnabled()) { + logger.debug("Default mapping to handler [" + resolvedHandler + "]"); + } + setDefaultHandler(resolvedHandler); + } + else { + this.handlerMap.put(urlPath, resolvedHandler); + if (logger.isDebugEnabled()) { + logger.debug("Mapped URL path [" + urlPath + "] onto handler [" + resolvedHandler + "]"); + } + } + } + } + + + /** + * Return the registered handlers as an unmodifiable Map, with the registered path + * as key and the handler object (or handler bean name in case of a lazy-init handler) + * as value. + * @see #getDefaultHandler() + */ + public final Map getHandlerMap() { + return Collections.unmodifiableMap(this.handlerMap); + } + + + /** + * Special interceptor for exposing the + * {@link AbstractUrlHandlerMapping#PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE} attribute. + * @link AbstractUrlHandlerMapping#exposePathWithinMapping + */ + private class PathExposingHandlerInterceptor extends HandlerInterceptorAdapter { + + private final String pathWithinMapping; + + public PathExposingHandlerInterceptor(String pathWithinMapping) { + this.pathWithinMapping = pathWithinMapping; + } + + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + exposePathWithinMapping(this.pathWithinMapping, request); + return true; + } + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/BeanNameUrlHandlerMapping.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/BeanNameUrlHandlerMapping.java new file mode 100644 index 00000000000..388a9c54b2f --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/BeanNameUrlHandlerMapping.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2008 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.web.servlet.handler; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.util.StringUtils; + +/** + * Implementation of the {@link org.springframework.web.servlet.HandlerMapping} + * interface that map from URLs to beans with names that start with a slash ("/"), + * similar to how Struts maps URLs to action names. + * + *

This is the default implementation used by the + * {@link org.springframework.web.servlet.DispatcherServlet}, along with + * {@link org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping} + * (on Java 5 and higher). Alternatively, {@link SimpleUrlHandlerMapping} allows for + * customizing a handler mapping declaratively. + * + *

The mapping is from URL to bean name. Thus an incoming URL "/foo" would map + * to a handler named "/foo", or to "/foo /foo2" in case of multiple mappings to + * a single handler. Note: In XML definitions, you'll need to use an alias + * name="/foo" in the bean definition, as the XML id may not contain slashes. + * + *

Supports direct matches (given "/test" -> registered "/test") and "*" + * matches (given "/test" -> registered "/t*"). Note that the default is + * to map within the current servlet mapping if applicable; see the + * {@link #setAlwaysUseFullPath "alwaysUseFullPath"} property for details. + * For details on the pattern options, see the + * {@link org.springframework.util.AntPathMatcher} javadoc. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see SimpleUrlHandlerMapping + */ +public class BeanNameUrlHandlerMapping extends AbstractDetectingUrlHandlerMapping { + + /** + * Checks name and aliases of the given bean for URLs, starting with "/". + */ + protected String[] determineUrlsForHandler(String beanName) { + List urls = new ArrayList(); + if (beanName.startsWith("/")) { + urls.add(beanName); + } + String[] aliases = getApplicationContext().getAliases(beanName); + for (int i = 0; i < aliases.length; i++) { + if (aliases[i].startsWith("/")) { + urls.add(aliases[i]); + } + } + return StringUtils.toStringArray(urls); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/DispatcherServletWebRequest.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/DispatcherServletWebRequest.java new file mode 100644 index 00000000000..920fcc36260 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/DispatcherServletWebRequest.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2008 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.web.servlet.handler; + +import java.util.Locale; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.servlet.support.RequestContextUtils; + +/** + * {@link ServletWebRequest} subclass that is aware of + * {@link org.springframework.web.servlet.DispatcherServlet}'s + * request context, such as the Locale determined by the configured + * {@link org.springframework.web.servlet.LocaleResolver}. + * + * @author Juergen Hoeller + * @since 2.0 + * @see #getLocale() + * @see org.springframework.web.servlet.LocaleResolver + */ +public class DispatcherServletWebRequest extends ServletWebRequest { + + /** + * Create a new DispatcherServletWebRequest instance for the given request. + * @param request current HTTP request + */ + public DispatcherServletWebRequest(HttpServletRequest request) { + super(request); + } + + public Locale getLocale() { + return RequestContextUtils.getLocale(getRequest()); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/HandlerInterceptorAdapter.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/HandlerInterceptorAdapter.java new file mode 100644 index 00000000000..03b04274a2d --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/HandlerInterceptorAdapter.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2006 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.web.servlet.handler; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; + +/** + * Abstract adapter class for the HandlerInterceptor interface, + * for simplified implementation of pre-only/post-only interceptors. + * + * @author Juergen Hoeller + * @since 05.12.2003 + */ +public abstract class HandlerInterceptorAdapter implements HandlerInterceptor { + + /** + * This implementation always returns true. + */ + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + return true; + } + + /** + * This implementation is empty. + */ + public void postHandle( + HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) + throws Exception { + } + + /** + * This implementation is empty. + */ + public void afterCompletion( + HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) + throws Exception { + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/SimpleMappingExceptionResolver.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/SimpleMappingExceptionResolver.java new file mode 100644 index 00000000000..1b42941e50f --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/SimpleMappingExceptionResolver.java @@ -0,0 +1,446 @@ +/* + * Copyright 2002-2008 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.web.servlet.handler; + +import java.util.Enumeration; +import java.util.Properties; +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.Ordered; +import org.springframework.web.servlet.HandlerExceptionResolver; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.util.WebUtils; + +/** + * {@link org.springframework.web.servlet.HandlerExceptionResolver} implementation + * that allows for mapping exception class names to view names, either for a + * set of given handlers or for all handlers in the DispatcherServlet. + * + *

Error views are analogous to error page JSPs, but can be used with any + * kind of exception including any checked one, with fine-granular mappings for + * specific handlers. + * + * @author Juergen Hoeller + * @since 22.11.2003 + * @see org.springframework.web.servlet.DispatcherServlet + */ +public class SimpleMappingExceptionResolver implements HandlerExceptionResolver, Ordered { + + /** + * The default name of the exception attribute: "exception". + */ + public static final String DEFAULT_EXCEPTION_ATTRIBUTE = "exception"; + + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + private int order = Integer.MAX_VALUE; // default: same as non-Ordered + + private Set mappedHandlers; + + private Class[] mappedHandlerClasses; + + private Log warnLogger; + + private Properties exceptionMappings; + + private String defaultErrorView; + + private Integer defaultStatusCode; + + private String exceptionAttribute = DEFAULT_EXCEPTION_ATTRIBUTE; + + + public void setOrder(int order) { + this.order = order; + } + + public int getOrder() { + return this.order; + } + + /** + * Specify the set of handlers that this exception resolver should apply to. + * The exception mappings and the default error view will only apply + * to the specified handlers. + *

If no handlers and handler classes are set, the exception mappings + * and the default error view will apply to all handlers. This means that + * a specified default error view will be used as fallback for all exceptions; + * any further HandlerExceptionResolvers in the chain will be ignored in + * this case. + */ + public void setMappedHandlers(Set mappedHandlers) { + this.mappedHandlers = mappedHandlers; + } + + /** + * Specify the set of classes that this exception resolver should apply to. + * The exception mappings and the default error view will only apply + * to handlers of the specified type; the specified types may be interfaces + * and superclasses of handlers as well. + *

If no handlers and handler classes are set, the exception mappings + * and the default error view will apply to all handlers. This means that + * a specified default error view will be used as fallback for all exceptions; + * any further HandlerExceptionResolvers in the chain will be ignored in + * this case. + */ + public void setMappedHandlerClasses(Class[] mappedHandlerClasses) { + this.mappedHandlerClasses = mappedHandlerClasses; + } + + /** + * Set the log category for warn logging. The name will be passed to the + * underlying logger implementation through Commons Logging, getting + * interpreted as log category according to the logger's configuration. + *

Default is no warn logging. Specify this setting to activate + * warn logging into a specific category. Alternatively, override + * the {@link #logException} method for custom logging. + * @see org.apache.commons.logging.LogFactory#getLog(String) + * @see org.apache.log4j.Logger#getLogger(String) + * @see java.util.logging.Logger#getLogger(String) + */ + public void setWarnLogCategory(String loggerName) { + this.warnLogger = LogFactory.getLog(loggerName); + } + + /** + * Set the mappings between exception class names and error view names. + * The exception class name can be a substring, with no wildcard support + * at present. A value of "ServletException" would match + * javax.servlet.ServletException and subclasses, for example. + *

NB: Consider carefully how specific the pattern is, and whether + * to include package information (which isn't mandatory). For example, + * "Exception" will match nearly anything, and will probably hide other rules. + * "java.lang.Exception" would be correct if "Exception" was meant to define + * a rule for all checked exceptions. With more unusual exception names such + * as "BaseBusinessException" there's no need to use a FQN. + *

Follows the same matching algorithm as RuleBasedTransactionAttribute + * and RollbackRuleAttribute. + * @param mappings exception patterns (can also be fully qualified class names) + * as keys, and error view names as values + * @see org.springframework.transaction.interceptor.RuleBasedTransactionAttribute + * @see org.springframework.transaction.interceptor.RollbackRuleAttribute + */ + public void setExceptionMappings(Properties mappings) { + this.exceptionMappings = mappings; + } + + /** + * Set the name of the default error view. + * This view will be returned if no specific mapping was found. + *

Default is none. + */ + public void setDefaultErrorView(String defaultErrorView) { + this.defaultErrorView = defaultErrorView; + } + + /** + * Set the default HTTP status code that this exception resolver will apply + * if it resolves an error view. + *

Note that this error code will only get applied in case of a top-level + * request. It will not be set for an include request, since the HTTP status + * cannot be modified from within an include. + *

If not specified, no status code will be applied, either leaving this to + * the controller or view, or keeping the servlet engine's default of 200 (OK). + * @param defaultStatusCode HTTP status code value, for example + * 500 (SC_INTERNAL_SERVER_ERROR) or 404 (SC_NOT_FOUND) + * @see javax.servlet.http.HttpServletResponse#SC_INTERNAL_SERVER_ERROR + * @see javax.servlet.http.HttpServletResponse#SC_NOT_FOUND + */ + public void setDefaultStatusCode(int defaultStatusCode) { + this.defaultStatusCode = new Integer(defaultStatusCode); + } + + /** + * Set the name of the model attribute as which the exception should + * be exposed. Default is "exception". + *

This can be either set to a different attribute name or to + * null for not exposing an exception attribute at all. + * @see #DEFAULT_EXCEPTION_ATTRIBUTE + */ + public void setExceptionAttribute(String exceptionAttribute) { + this.exceptionAttribute = exceptionAttribute; + } + + + /** + * Checks whether this resolver is supposed to apply (i.e. the handler + * matches in case of "mappedHandlers" having been specified), then + * delegates to the {@link #doResolveException} template method. + */ + public ModelAndView resolveException( + HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { + + if (shouldApplyTo(request, handler)) { + return doResolveException(request, response, handler, ex); + } + else { + return null; + } + } + + /** + * Check whether this resolver is supposed to apply to the given handler. + *

The default implementation checks against the specified mapped handlers + * and handler classes, if any. + * @param request current HTTP request + * @param handler the executed handler, or null if none chosen at the + * time of the exception (for example, if multipart resolution failed) + * @return whether this resolved should proceed with resolving the exception + * for the given request and handler + * @see #setMappedHandlers + * @see #setMappedHandlerClasses + */ + protected boolean shouldApplyTo(HttpServletRequest request, Object handler) { + if (handler != null) { + if (this.mappedHandlers != null && this.mappedHandlers.contains(handler)) { + return true; + } + if (this.mappedHandlerClasses != null) { + for (int i = 0; i < this.mappedHandlerClasses.length; i++) { + if (this.mappedHandlerClasses[i].isInstance(handler)) { + return true; + } + } + } + } + // Else only apply if there are no explicit handler mappings. + return (this.mappedHandlers == null && this.mappedHandlerClasses == null); + } + + /** + * Actually resolve the given exception that got thrown during on handler execution, + * returning a ModelAndView that represents a specific error page if appropriate. + *

May be overridden in subclasses, in order to apply specific exception checks. + * Note that this template method will be invoked after checking whether + * this resolved applies ("mappedHandlers" etc), so an implementation may simply + * proceed with its actual exception handling. + * @param request current HTTP request + * @param response current HTTP response + * @param handler the executed handler, or null if none chosen at the + * time of the exception (for example, if multipart resolution failed) + * @param ex the exception that got thrown during handler execution + * @return a corresponding ModelAndView to forward to, or null for default processing + */ + protected ModelAndView doResolveException( + HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { + + // Log exception, both at debug log level and at warn level, if desired. + if (logger.isDebugEnabled()) { + logger.debug("Resolving exception from handler [" + handler + "]: " + ex); + } + logException(ex, request); + + // Expose ModelAndView for chosen error view. + String viewName = determineViewName(ex, request); + if (viewName != null) { + // Apply HTTP status code for error views, if specified. + // Only apply it if we're processing a top-level request. + Integer statusCode = determineStatusCode(request, viewName); + if (statusCode != null) { + applyStatusCodeIfPossible(request, response, statusCode.intValue()); + } + return getModelAndView(viewName, ex, request); + } + else { + return null; + } + } + + + /** + * Log the given exception at warn level, provided that warn logging has been + * activated through the {@link #setWarnLogCategory "warnLogCategory"} property. + *

Calls {@link #buildLogMessage} in order to determine the concrete message + * to log. Always passes the full exception to the logger. + * @param ex the exception that got thrown during handler execution + * @param request current HTTP request (useful for obtaining metadata) + * @see #setWarnLogCategory + * @see #buildLogMessage + * @see org.apache.commons.logging.Log#warn(Object, Throwable) + */ + protected void logException(Exception ex, HttpServletRequest request) { + if (this.warnLogger != null && this.warnLogger.isWarnEnabled()) { + this.warnLogger.warn(buildLogMessage(ex, request), ex); + } + } + + /** + * Build a log message for the given exception, occured during processing + * the given request. + * @param ex the exception that got thrown during handler execution + * @param request current HTTP request (useful for obtaining metadata) + * @return the log message to use + */ + protected String buildLogMessage(Exception ex, HttpServletRequest request) { + return "Handler execution resulted in exception"; + } + + + /** + * Determine the view name for the given exception, searching the + * {@link #setExceptionMappings "exceptionMappings"}, using the + * {@link #setDefaultErrorView "defaultErrorView"} as fallback. + * @param ex the exception that got thrown during handler execution + * @param request current HTTP request (useful for obtaining metadata) + * @return the resolved view name, or null if none found + */ + protected String determineViewName(Exception ex, HttpServletRequest request) { + String viewName = null; + // Check for specific exception mappings. + if (this.exceptionMappings != null) { + viewName = findMatchingViewName(this.exceptionMappings, ex); + } + // Return default error view else, if defined. + if (viewName == null && this.defaultErrorView != null) { + if (logger.isDebugEnabled()) { + logger.debug("Resolving to default view '" + this.defaultErrorView + + "' for exception of type [" + ex.getClass().getName() + "]"); + } + viewName = this.defaultErrorView; + } + return viewName; + } + + /** + * Find a matching view name in the given exception mappings. + * @param exceptionMappings mappings between exception class names and error view names + * @param ex the exception that got thrown during handler execution + * @return the view name, or null if none found + * @see #setExceptionMappings + */ + protected String findMatchingViewName(Properties exceptionMappings, Exception ex) { + String viewName = null; + String dominantMapping = null; + int deepest = Integer.MAX_VALUE; + for (Enumeration names = exceptionMappings.propertyNames(); names.hasMoreElements();) { + String exceptionMapping = (String) names.nextElement(); + int depth = getDepth(exceptionMapping, ex); + if (depth >= 0 && depth < deepest) { + deepest = depth; + dominantMapping = exceptionMapping; + viewName = exceptionMappings.getProperty(exceptionMapping); + } + } + if (viewName != null && logger.isDebugEnabled()) { + logger.debug("Resolving to view '" + viewName + "' for exception of type [" + ex.getClass().getName() + + "], based on exception mapping [" + dominantMapping + "]"); + } + return viewName; + } + + /** + * Return the depth to the superclass matching. + *

0 means ex matches exactly. Returns -1 if there's no match. + * Otherwise, returns depth. Lowest depth wins. + *

Follows the same algorithm as + * {@link org.springframework.transaction.interceptor.RollbackRuleAttribute}. + */ + protected int getDepth(String exceptionMapping, Exception ex) { + return getDepth(exceptionMapping, ex.getClass(), 0); + } + + private int getDepth(String exceptionMapping, Class exceptionClass, int depth) { + if (exceptionClass.getName().indexOf(exceptionMapping) != -1) { + // Found it! + return depth; + } + // If we've gone as far as we can go and haven't found it... + if (exceptionClass.equals(Throwable.class)) { + return -1; + } + return getDepth(exceptionMapping, exceptionClass.getSuperclass(), depth + 1); + } + + + /** + * Determine the HTTP status code to apply for the given error view. + *

The default implementation always returns the specified + * {@link #setDefaultStatusCode "defaultStatusCode"}, as a common + * status code for all error views. Override this in a custom subclass + * to determine a specific status code for the given view. + * @param request current HTTP request + * @param viewName the name of the error view + * @return the HTTP status code to use, or null for the + * servlet container's default (200 in case of a standard error view) + * @see #setDefaultStatusCode + * @see #applyStatusCodeIfPossible + */ + protected Integer determineStatusCode(HttpServletRequest request, String viewName) { + return this.defaultStatusCode; + } + + /** + * Apply the specified HTTP status code to the given response, if possible + * (that is, if not executing within an include request). + * @param request current HTTP request + * @param response current HTTP response + * @param statusCode the status code to apply + * @see #determineStatusCode + * @see #setDefaultStatusCode + * @see javax.servlet.http.HttpServletResponse#setStatus + */ + protected void applyStatusCodeIfPossible(HttpServletRequest request, HttpServletResponse response, int statusCode) { + if (!WebUtils.isIncludeRequest(request)) { + if (logger.isDebugEnabled()) { + logger.debug("Applying HTTP status code " + statusCode); + } + response.setStatus(statusCode); + request.setAttribute(WebUtils.ERROR_STATUS_CODE_ATTRIBUTE, new Integer(statusCode)); + } + } + + /** + * Return a ModelAndView for the given request, view name and exception. + *

The default implementation delegates to {@link #getModelAndView(String, Exception)}. + * @param viewName the name of the error view + * @param ex the exception that got thrown during handler execution + * @param request current HTTP request (useful for obtaining metadata) + * @return the ModelAndView instance + */ + protected ModelAndView getModelAndView(String viewName, Exception ex, HttpServletRequest request) { + return getModelAndView(viewName, ex); + } + + /** + * Return a ModelAndView for the given view name and exception. + *

The default implementation adds the specified exception attribute. + * Can be overridden in subclasses. + * @param viewName the name of the error view + * @param ex the exception that got thrown during handler execution + * @return the ModelAndView instance + * @see #setExceptionAttribute + */ + protected ModelAndView getModelAndView(String viewName, Exception ex) { + ModelAndView mv = new ModelAndView(viewName); + if (this.exceptionAttribute != null) { + if (logger.isDebugEnabled()) { + logger.debug("Exposing Exception as model attribute '" + this.exceptionAttribute + "'"); + } + mv.addObject(this.exceptionAttribute, ex); + } + return mv; + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/SimpleServletHandlerAdapter.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/SimpleServletHandlerAdapter.java new file mode 100644 index 00000000000..189e674d75f --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/SimpleServletHandlerAdapter.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2005 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.web.servlet.handler; + +import javax.servlet.Servlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.web.servlet.HandlerAdapter; +import org.springframework.web.servlet.ModelAndView; + +/** + * Adapter to use the Servlet interface with the generic DispatcherServlet. + * Calls the Servlet's service method to handle a request. + * + *

Last-modified checking is not explicitly supported: This is typically + * handled by the Servlet implementation itself (usually deriving from + * the HttpServlet base class). + * + *

This adapter is not activated by default; it needs to be defined as a + * bean in the DispatcherServlet context. It will automatically apply to + * mapped handler beans that implement the Servlet interface then. + * + *

Note that Servlet instances defined as bean will not receive initialization + * and destruction callbacks, unless a special post-processor such as + * SimpleServletPostProcessor is defined in the DispatcherServlet context. + * + *

Alternatively, consider wrapping a Servlet with Spring's + * ServletWrappingController. This is particularly appropriate for + * existing Servlet classes, allowing to specify Servlet initialization + * parameters etc. + * + * @author Juergen Hoeller + * @since 1.1.5 + * @see javax.servlet.Servlet + * @see javax.servlet.http.HttpServlet + * @see SimpleServletPostProcessor + * @see org.springframework.web.servlet.mvc.ServletWrappingController + */ +public class SimpleServletHandlerAdapter implements HandlerAdapter { + + public boolean supports(Object handler) { + return (handler instanceof Servlet); + } + + public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + + ((Servlet) handler).service(request, response); + return null; + } + + public long getLastModified(HttpServletRequest request, Object handler) { + return -1; + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/SimpleServletPostProcessor.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/SimpleServletPostProcessor.java new file mode 100644 index 00000000000..37b7dddeb90 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/SimpleServletPostProcessor.java @@ -0,0 +1,157 @@ +/* + * Copyright 2002-2007 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.web.servlet.handler; + +import java.util.Collections; +import java.util.Enumeration; + +import javax.servlet.Servlet; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor; +import org.springframework.web.context.ServletConfigAware; +import org.springframework.web.context.ServletContextAware; + +/** + * {@link org.springframework.beans.factory.config.BeanPostProcessor} + * that applies initialization and destruction callbacks to beans that + * implement the {@link javax.servlet.Servlet} interface. + * + *

After initialization of the bean instance, the Servlet init + * method will be called with a ServletConfig that contains the bean name + * of the Servlet and the ServletContext that it is running in. + * + *

Before destruction of the bean instance, the Servlet destroy + * will be called. + * + *

Note that this post-processor does not support Servlet initialization + * parameters. Bean instances that implement the Servlet interface are + * supposed to be configured like any other Spring bean, that is, through + * constructor arguments or bean properties. + * + *

For reuse of a Servlet implementation in a plain Servlet container + * and as a bean in a Spring context, consider deriving from Spring's + * {@link org.springframework.web.servlet.HttpServletBean} base class that + * applies Servlet initialization parameters as bean properties, supporting + * both the standard Servlet and the Spring bean initialization style. + * + *

Alternatively, consider wrapping a Servlet with Spring's + * {@link org.springframework.web.servlet.mvc.ServletWrappingController}. + * This is particularly appropriate for existing Servlet classes, + * allowing to specify Servlet initialization parameters etc. + * + * @author Juergen Hoeller + * @since 1.1.5 + * @see javax.servlet.Servlet#init(javax.servlet.ServletConfig) + * @see javax.servlet.Servlet#destroy() + * @see SimpleServletHandlerAdapter + */ +public class SimpleServletPostProcessor implements + DestructionAwareBeanPostProcessor, ServletContextAware, ServletConfigAware { + + private boolean useSharedServletConfig = true; + + private ServletContext servletContext; + + private ServletConfig servletConfig; + + + /** + * Set whether to use the shared ServletConfig object passed in + * through setServletConfig, if available. + *

Default is "true". Turn this setting to "false" to pass in + * a mock ServletConfig object with the bean name as servlet name, + * holding the current ServletContext. + * @see #setServletConfig + */ + public void setUseSharedServletConfig(boolean useSharedServletConfig) { + this.useSharedServletConfig = useSharedServletConfig; + } + + public void setServletContext(ServletContext servletContext) { + this.servletContext = servletContext; + } + + public void setServletConfig(ServletConfig servletConfig) { + this.servletConfig = servletConfig; + } + + + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + return bean; + } + + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof Servlet) { + ServletConfig config = this.servletConfig; + if (config == null || !this.useSharedServletConfig) { + config = new DelegatingServletConfig(beanName, this.servletContext); + } + try { + ((Servlet) bean).init(config); + } + catch (ServletException ex) { + throw new BeanInitializationException("Servlet.init threw exception", ex); + } + } + return bean; + } + + public void postProcessBeforeDestruction(Object bean, String beanName) throws BeansException { + if (bean instanceof Servlet) { + ((Servlet) bean).destroy(); + } + } + + + /** + * Internal implementation of the {@link ServletConfig} interface, + * to be passed to the wrapped servlet. + */ + private static class DelegatingServletConfig implements ServletConfig { + + private final String servletName; + + private final ServletContext servletContext; + + public DelegatingServletConfig(String servletName, ServletContext servletContext) { + this.servletName = servletName; + this.servletContext = servletContext; + } + + public String getServletName() { + return this.servletName; + } + + public ServletContext getServletContext() { + return this.servletContext; + } + + public String getInitParameter(String paramName) { + return null; + } + + public Enumeration getInitParameterNames() { + return Collections.enumeration(Collections.EMPTY_SET); + } + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/SimpleUrlHandlerMapping.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/SimpleUrlHandlerMapping.java new file mode 100644 index 00000000000..e6a5c6619f1 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/SimpleUrlHandlerMapping.java @@ -0,0 +1,134 @@ +/* + * Copyright 2002-2007 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.web.servlet.handler; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Properties; + +import org.springframework.beans.BeansException; + +/** + * Implementation of the {@link org.springframework.web.servlet.HandlerMapping} + * interface to map from URLs to request handler beans. Supports both mapping to bean + * instances and mapping to bean names; the latter is required for non-singleton handlers. + * + *

The "urlMap" property is suitable for populating the handler map with + * bean references, e.g. via the map element in XML bean definitions. + * + *

Mappings to bean names can be set via the "mappings" property, in a form + * accepted by the java.util.Properties class, like as follows:
+ * + * /welcome.html=ticketController + * /show.html=ticketController + *
+ * The syntax is PATH=HANDLER_BEAN_NAME. + * If the path doesn't begin with a slash, one is prepended. + * + *

Supports direct matches (given "/test" -> registered "/test") and "*" + * matches (given "/test" -> registered "/t*"). Note that the default is + * to map within the current servlet mapping if applicable; see the + * {@link #setAlwaysUseFullPath "alwaysUseFullPath"} property for details. + * For details on the pattern options, see the + * {@link org.springframework.util.AntPathMatcher} javadoc. + + * @author Rod Johnson + * @author Juergen Hoeller + * @see #setMappings + * @see #setUrlMap + * @see BeanNameUrlHandlerMapping + */ +public class SimpleUrlHandlerMapping extends AbstractUrlHandlerMapping { + + private final Map urlMap = new HashMap(); + + + /** + * Map URL paths to handler bean names. + * This is the typical way of configuring this HandlerMapping. + *

Supports direct URL matches and Ant-style pattern matches. For syntax + * details, see the {@link org.springframework.util.AntPathMatcher} javadoc. + * @param mappings properties with URLs as keys and bean names as values + * @see #setUrlMap + */ + public void setMappings(Properties mappings) { + this.urlMap.putAll(mappings); + } + + /** + * Set a Map with URL paths as keys and handler beans (or handler bean names) + * as values. Convenient for population with bean references. + *

Supports direct URL matches and Ant-style pattern matches. For syntax + * details, see the {@link org.springframework.util.AntPathMatcher} javadoc. + * @param urlMap map with URLs as keys and beans as values + * @see #setMappings + */ + public void setUrlMap(Map urlMap) { + this.urlMap.putAll(urlMap); + } + + /** + * Allow Map access to the URL path mappings, with the option to add or + * override specific entries. + *

Useful for specifying entries directly, for example via "urlMap[myKey]". + * This is particularly useful for adding or overriding entries in child + * bean definitions. + */ + public Map getUrlMap() { + return this.urlMap; + } + + + /** + * Calls the {@link #registerHandlers} method in addition to the + * superclass's initialization. + */ + public void initApplicationContext() throws BeansException { + super.initApplicationContext(); + registerHandlers(this.urlMap); + } + + /** + * Register all handlers specified in the URL map for the corresponding paths. + * @param urlMap Map with URL paths as keys and handler beans or bean names as values + * @throws BeansException if a handler couldn't be registered + * @throws IllegalStateException if there is a conflicting handler registered + */ + protected void registerHandlers(Map urlMap) throws BeansException { + if (urlMap.isEmpty()) { + logger.warn("Neither 'urlMap' nor 'mappings' set on SimpleUrlHandlerMapping"); + } + else { + Iterator it = urlMap.keySet().iterator(); + while (it.hasNext()) { + String url = (String) it.next(); + Object handler = urlMap.get(url); + // Prepend with slash if not already present. + if (!url.startsWith("/")) { + url = "/" + url; + } + // Remove whitespace from handler bean name. + if (handler instanceof String) { + handler = ((String) handler).trim(); + } + registerHandler(url, handler); + } + } + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/UserRoleAuthorizationInterceptor.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/UserRoleAuthorizationInterceptor.java new file mode 100644 index 00000000000..1144b368b1e --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/UserRoleAuthorizationInterceptor.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2006 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.web.servlet.handler; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Interceptor that checks the authorization of the current user via the + * user's roles, as evaluated by HttpServletRequest's isUserInRole method. + * + * @author Juergen Hoeller + * @since 20.06.2003 + * @see javax.servlet.http.HttpServletRequest#isUserInRole + */ +public class UserRoleAuthorizationInterceptor extends HandlerInterceptorAdapter { + + private String[] authorizedRoles; + + + /** + * Set the roles that this interceptor should treat as authorized. + * @param authorizedRoles array of role names + */ + public final void setAuthorizedRoles(String[] authorizedRoles) { + this.authorizedRoles = authorizedRoles; + } + + + public final boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws ServletException, IOException { + + if (this.authorizedRoles != null) { + for (int i = 0; i < this.authorizedRoles.length; i++) { + if (request.isUserInRole(this.authorizedRoles[i])) { + return true; + } + } + } + handleNotAuthorized(request, response, handler); + return false; + } + + /** + * Handle a request that is not authorized according to this interceptor. + * Default implementation sends HTTP status code 403 ("forbidden"). + *

This method can be overridden to write a custom message, forward or + * redirect to some error page or login page, or throw a ServletException. + * @param request current HTTP request + * @param response current HTTP response + * @param handler chosen handler to execute, for type and/or instance evaluation + * @throws javax.servlet.ServletException if there is an internal error + * @throws java.io.IOException in case of an I/O error when writing the response + */ + protected void handleNotAuthorized(HttpServletRequest request, HttpServletResponse response, Object handler) + throws ServletException, IOException { + + response.sendError(HttpServletResponse.SC_FORBIDDEN); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/WebRequestHandlerInterceptorAdapter.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/WebRequestHandlerInterceptorAdapter.java new file mode 100644 index 00000000000..b1f5ea31815 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/WebRequestHandlerInterceptorAdapter.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2006 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.web.servlet.handler; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.util.Assert; +import org.springframework.web.context.request.WebRequestInterceptor; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; + +/** + * Adapter that implements the Servlet HandlerInterceptor interface + * and wraps an underlying WebRequestInterceptor. + * + * @author Juergen Hoeller + * @since 2.0 + * @see org.springframework.web.context.request.WebRequestInterceptor + * @see org.springframework.web.servlet.HandlerInterceptor + */ +public class WebRequestHandlerInterceptorAdapter implements HandlerInterceptor { + + private final WebRequestInterceptor requestInterceptor; + + + /** + * Create a new WebRequestHandlerInterceptorAdapter for the given WebRequestInterceptor. + * @param requestInterceptor the WebRequestInterceptor to wrap + */ + public WebRequestHandlerInterceptorAdapter(WebRequestInterceptor requestInterceptor) { + Assert.notNull(requestInterceptor, "WebRequestInterceptor must not be null"); + this.requestInterceptor = requestInterceptor; + } + + + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + + this.requestInterceptor.preHandle(new DispatcherServletWebRequest(request)); + return true; + } + + public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) + throws Exception { + + this.requestInterceptor.postHandle(new DispatcherServletWebRequest(request), + (modelAndView != null ? modelAndView.getModelMap() : null)); + } + + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) + throws Exception { + + this.requestInterceptor.afterCompletion(new DispatcherServletWebRequest(request), ex); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/metadata/AbstractPathMapHandlerMapping.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/metadata/AbstractPathMapHandlerMapping.java new file mode 100644 index 00000000000..25fed95c6f5 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/metadata/AbstractPathMapHandlerMapping.java @@ -0,0 +1,201 @@ +/* + * Copyright 2002-2008 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.web.servlet.handler.metadata; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.Constants; +import org.springframework.web.servlet.handler.AbstractUrlHandlerMapping; + +/** + * Abstract implementation of the HandlerMapping interface that recognizes + * metadata attributes of type PathMap on application Controllers and automatically + * wires them into the current servlet's WebApplicationContext. + * + *

The path must be mapped to the relevant Spring DispatcherServlet in /WEB-INF/web.xml. + * It's possible to have multiple PathMap attributes on the one controller class. + * + *

Controllers instantiated by this class may have dependencies on middle tier + * objects, expressed via JavaBean properties or constructor arguments. These will + * be resolved automatically. + * + *

You will normally use this HandlerMapping with at most one DispatcherServlet in your + * web application. Otherwise you'll end with one instance of the mapped controller for + * each DispatcherServlet's context. You might want this -- for example, if + * one's using a .pdf mapping and a PDF view, and another a JSP view, or if + * using different middle tier objects, but should understand the implications. All + * Controllers with attributes will be picked up by each DispatcherServlet's context. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @deprecated as of Spring 2.5, in favor of annotation-based request mapping. + * To be removed in Spring 3.0. + */ +public abstract class AbstractPathMapHandlerMapping extends AbstractUrlHandlerMapping { + + /** Constants instance for AutowireCapableBeanFactory */ + private static final Constants constants = new Constants(AutowireCapableBeanFactory.class); + + private int autowireMode = AutowireCapableBeanFactory.AUTOWIRE_AUTODETECT; + + private boolean dependencyCheck = true; + + + /** + * Set the autowire mode for handlers, by the name of the corresponding constant + * in the AutowireCapableBeanFactory interface, e.g. "AUTOWIRE_BY_NAME". + * @param constantName name of the constant + * @throws java.lang.IllegalArgumentException if an invalid constant was specified + * @see #setAutowireMode + * @see org.springframework.beans.factory.config.AutowireCapableBeanFactory#AUTOWIRE_BY_NAME + * @see org.springframework.beans.factory.config.AutowireCapableBeanFactory#AUTOWIRE_BY_TYPE + * @see org.springframework.beans.factory.config.AutowireCapableBeanFactory#AUTOWIRE_CONSTRUCTOR + * @see org.springframework.beans.factory.config.AutowireCapableBeanFactory#AUTOWIRE_AUTODETECT + */ + public void setAutowireModeName(String constantName) throws IllegalArgumentException { + setAutowireMode(constants.asNumber(constantName).intValue()); + } + + /** + * Set the autowire mode for handlers. This determines whether any automagical + * detection and setting of bean references will happen. + *

Default is AUTOWIRE_AUTODETECT, which means either constructor autowiring or + * autowiring by type (depending on the constructors available in the class). + * @param autowireMode the autowire mode to set. + * Must be one of the constants defined in the AutowireCapableBeanFactory interface. + * @see org.springframework.beans.factory.config.AutowireCapableBeanFactory#AUTOWIRE_BY_NAME + * @see org.springframework.beans.factory.config.AutowireCapableBeanFactory#AUTOWIRE_BY_TYPE + * @see org.springframework.beans.factory.config.AutowireCapableBeanFactory#AUTOWIRE_CONSTRUCTOR + * @see org.springframework.beans.factory.config.AutowireCapableBeanFactory#AUTOWIRE_AUTODETECT + */ + public void setAutowireMode(int autowireMode) { + this.autowireMode = autowireMode; + } + + /** + * Set whether to perform a dependency check for objects on autowired handlers. + * Not applicable to autowiring a constructor, thus ignored there. + *

Default is "true". + */ + public void setDependencyCheck(boolean dependencyCheck) { + this.dependencyCheck = dependencyCheck; + } + + + /** + * Calls the detectAndCreateHandlers method in addition + * to the superclass's initialization. + * @see #detectAndCreateHandlers + */ + public void initApplicationContext() throws BeansException { + super.initApplicationContext(); + + if (!(getApplicationContext() instanceof ConfigurableApplicationContext)) { + throw new IllegalStateException( + "[" + getClass().getName() + "] needs to run in a ConfigurableApplicationContext"); + } + ConfigurableListableBeanFactory beanFactory = + ((ConfigurableApplicationContext) getApplicationContext()).getBeanFactory(); + detectAndCreateHandlers(beanFactory); + } + + /** + * Look for all classes with a PathMap class attribute, instantiate them in + * the owning ApplicationContext, and register them as MVC handlers usable + * by the current DispatcherServlet. + * @param beanFactory the ConfigurableListableBeanFactory to register the + * created handler instances with + * @throws BeansException if handler detection or creation failed + * @see PathMap + * @see #getClassesWithPathMapAttributes() + * @see org.springframework.beans.factory.config.ConfigurableListableBeanFactory#createBean + * @see org.springframework.beans.factory.config.ConfigurableListableBeanFactory#registerSingleton + */ + protected void detectAndCreateHandlers(ConfigurableListableBeanFactory beanFactory) throws BeansException { + try { + Class[] handlerClasses = getClassesWithPathMapAttributes(); + if (logger.isDebugEnabled()) { + logger.debug("Found " + handlerClasses.length + " attribute-targeted handlers"); + } + + // for each Class returned by the Commons Attribute indexer + for (int i = 0; i < handlerClasses.length; i++) { + Class handlerClass = handlerClasses[i]; + + // Autowire the given handler class via AutowireCapableBeanFactory. + // Either autowires a constructor or by type, depending on the + // constructors available in the given class. + Object handler = beanFactory.createBean(handlerClass, this.autowireMode, this.dependencyCheck); + + // We now have an "autowired" handler, that may reference beans in the + // application context. We now add the new handler to the factory. + // This isn't necessary for the handler to work, but is useful if we want + // to enumerate controllers in the factory etc. + beanFactory.registerSingleton(handlerClass.getName(), handler); + + // There may be multiple paths mapped to this handler. + PathMap[] pathMaps = getPathMapAttributes(handlerClass); + registerHandler(pathMaps, handler); + } + } + catch (BeansException ex) { + throw ex; + } + catch (Exception ex) { + throw new BeanInitializationException("Could not retrieve PathMap attributes", ex); + } + } + + /** + * Register the given handler for the URL paths indicated by the given PathMaps. + * @param pathMaps the PathMap attributes for the handler class + * @param handler the handler instance + * @throws BeansException if the handler couldn't be registered + * @throws IllegalStateException if there is a conflicting handler registered + */ + protected void registerHandler(PathMap[] pathMaps, Object handler) throws BeansException, IllegalStateException { + for (int j = 0; j < pathMaps.length; j++) { + PathMap pathMap = pathMaps[j]; + String path = pathMap.getUrl(); + if (!path.startsWith("/")) { + path = "/" + path; + } + registerHandler(path, handler); + } + } + + + /** + * Use an attribute index to get a Collection of Class objects + * with the required PathMap attribute. + * @return a array of Class objects + */ + protected abstract Class[] getClassesWithPathMapAttributes() throws Exception; + + /** + * Use Attributes API to find PathMap attributes for the given handler class. + * We know there's at least one, as the getClassNamesWithPathMapAttributes + * method return this class name. + * @param handlerClass the handler class to look for + * @return an array of PathMap objects + */ + protected abstract PathMap[] getPathMapAttributes(Class handlerClass) throws Exception; + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/metadata/CommonsPathMapHandlerMapping.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/metadata/CommonsPathMapHandlerMapping.java new file mode 100644 index 00000000000..bcefb9a56a2 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/metadata/CommonsPathMapHandlerMapping.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2008 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.web.servlet.handler.metadata; + +import java.util.Collection; + +import org.apache.commons.attributes.AttributeIndex; +import org.apache.commons.attributes.Attributes; + +/** + * Subclass of AbstractPathMapHandlerMapping that recognizes Commons Attributes + * metadata attributes of type PathMap on application Controllers and automatically + * wires them into the current servlet's WebApplicationContext. + * + *

+ * Controllers must have class attributes of the form: + * + * &64;org.springframework.web.servlet.handler.commonsattributes.PathMap("/path.cgi") + * + * + *

The path must be mapped to the relevant Spring DispatcherServlet in /WEB-INF/web.xml. + * It's possible to have multiple PathMap attributes on the one controller class. + * + *

To use this feature, you must compile application classes with Commons Attributes, + * and run the Commons Attributes indexer tool on your application classes, which must + * be in a Jar rather than in WEB-INF/classes. + * + *

Controllers instantiated by this class may have dependencies on middle tier + * objects, expressed via JavaBean properties or constructor arguments. These will + * be resolved automatically. + * + *

You will normally use this HandlerMapping with at most one DispatcherServlet in + * your web application. Otherwise you'll end with one instance of the mapped controller + * for each DispatcherServlet's context. You might want this--for example, if + * one's using a .pdf mapping and a PDF view, and another a JSP view, or if using + * different middle tier objects, but should understand the implications. All + * Controllers with attributes will be picked up by each DispatcherServlet's context. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @deprecated as of Spring 2.5, in favor of annotation-based request mapping. + * To be removed in Spring 3.0. + */ +public class CommonsPathMapHandlerMapping extends AbstractPathMapHandlerMapping { + + /** + * Use Commons Attributes AttributeIndex to get a Collection of Class + * objects with the required PathMap attribute. Protected so that it can + * be overridden during testing. + */ + protected Class[] getClassesWithPathMapAttributes() throws Exception { + AttributeIndex ai = new AttributeIndex(getClass().getClassLoader()); + Collection classes = ai.getClasses(PathMap.class); + return (Class[]) classes.toArray(new Class[classes.size()]); + } + + /** + * Use Commons Attributes to find PathMap attributes for the given class. + * We know there's at least one, as the getClassNamesWithPathMapAttributes + * method return this class name. + */ + protected PathMap[] getPathMapAttributes(Class handlerClass) { + Collection atts = Attributes.getAttributes(handlerClass, PathMap.class); + return (PathMap[]) atts.toArray(new PathMap[atts.size()]); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/metadata/PathMap.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/metadata/PathMap.java new file mode 100644 index 00000000000..f838094843c --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/metadata/PathMap.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2008 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.web.servlet.handler.metadata; + +/** + * Attribute to be used on Controller classes to allow for automatic URL mapping + * without web controllers being defined as beans in an XML bean definition file. + * + *

The path map should be the path in the current application, such as /foo.cgi. + * If there is no leading "/", one will be prepended. + * + *

Application code must use the Commons Attributes indexer tool to use this option. + * + * @author Rod Johnson + * @deprecated as of Spring 2.5, in favor of annotation-based request mapping. + * To be removed in Spring 3.0. + * @@org.apache.commons.attributes.Indexed() + */ +public class PathMap { + + /* + * NB: The Indexed attribute on this class is required. Thus the Spring jar + * must be built including a Commons Attributes attribute compilation step + * for this class. + */ + + private final String url; + + + /** + * Create a new PathMap attribute for the given URL. + */ + public PathMap(String url) { + this.url = url; + } + + /** + * Return the URL that this attribute indicates. + */ + public String getUrl() { + return this.url; + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/metadata/package.html b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/metadata/package.html new file mode 100644 index 00000000000..6ffef6dc50e --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/metadata/package.html @@ -0,0 +1,37 @@ + + + +This package enables automatic web controller targeting. + +The only implementation at present is CommonsPathMapHandlerMapping, based on Commons +Attributes source-level attributes, but metadata implementation is pluggable. +
+In this model, you don't need to map URLs onto controllers in your Spring +XML web application context bean definition files. You merely need to +set one or more PathMap attributes on each of your Controller classes, and +new objects of these types will automatically be added to the relevant +web application context. +
+Dependencies--whether expressed by via constructor arguments or JavaBean +properties, will be resolved if possible using the middle tier definitions in +the WebApplicationContext. + +

+To use this feature, using the Commons Attributes implementation, perform the following steps: +

    +
  1. Invoke the Commons Attributes compiler on your application classes. The + classes must be compiled into a Jar rather than the WEB-INF/classes directory. +
  2. Run the Commons Attributes attribute indexer tool on the Jar containing + your controllers. +
  3. Use Commons Attributes syntax to define a PathMap attribute for each + Controller you want mapped. +
  4. Define a bean of type UrlHandlerMapping in your servletName-servlet.xml + bean definition file in your web application. No parameters are required. +
+ +

+You can also use other HandlerMappings, such as BeanNameHandlerMapping, in the +same servlet XML file. + + + diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/package.html b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/package.html new file mode 100644 index 00000000000..56ea63f53ed --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/package.html @@ -0,0 +1,8 @@ + + + +Provides standard HandlerMapping implementations, +including abstract base classes for custom implementations. + + + diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/i18n/AbstractLocaleResolver.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/i18n/AbstractLocaleResolver.java new file mode 100644 index 00000000000..885688036fd --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/i18n/AbstractLocaleResolver.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2007 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.web.servlet.i18n; + +import java.util.Locale; + +import org.springframework.web.servlet.LocaleResolver; + +/** + * Abstract base class for {@link LocaleResolver} implementations. + * Provides support for a default locale. + * + * @author Juergen Hoeller + * @since 1.2.9 + */ +public abstract class AbstractLocaleResolver implements LocaleResolver { + + private Locale defaultLocale; + + + /** + * Set a default Locale that this resolver will return if no other locale found. + */ + public void setDefaultLocale(Locale defaultLocale) { + this.defaultLocale = defaultLocale; + } + + /** + * Return the default Locale that this resolver is supposed to fall back to, if any. + */ + protected Locale getDefaultLocale() { + return this.defaultLocale; + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/i18n/AcceptHeaderLocaleResolver.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/i18n/AcceptHeaderLocaleResolver.java new file mode 100644 index 00000000000..638133a71e0 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/i18n/AcceptHeaderLocaleResolver.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2006 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.web.servlet.i18n; + +import java.util.Locale; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.web.servlet.LocaleResolver; + +/** + * Implementation of LocaleResolver that simply uses the primary locale + * specified in the "accept-language" header of the HTTP request (that is, + * the locale sent by the client browser, normally that of the client's OS). + * + *

Note: Does not support setLocale, since the accept header + * can only be changed through changing the client's locale settings. + * + * @author Juergen Hoeller + * @since 27.02.2003 + * @see javax.servlet.http.HttpServletRequest#getLocale() + */ +public class AcceptHeaderLocaleResolver implements LocaleResolver { + + public Locale resolveLocale(HttpServletRequest request) { + return request.getLocale(); + } + + public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) { + throw new UnsupportedOperationException( + "Cannot change HTTP accept header - use a different locale resolution strategy"); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/i18n/CookieLocaleResolver.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/i18n/CookieLocaleResolver.java new file mode 100644 index 00000000000..5cc25460f24 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/i18n/CookieLocaleResolver.java @@ -0,0 +1,146 @@ +/* + * Copyright 2002-2007 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.web.servlet.i18n; + +import java.util.Locale; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.util.StringUtils; +import org.springframework.web.servlet.LocaleResolver; +import org.springframework.web.util.CookieGenerator; +import org.springframework.web.util.WebUtils; + +/** + * {@link LocaleResolver} implementation that uses a cookie sent back to the user + * in case of a custom setting, with a fallback to the specified default locale + * or the request's accept-header locale. + * + *

This is particularly useful for stateless applications without user sessions. + * + *

Custom controllers can thus override the user's locale by calling + * {@link #setLocale(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, java.util.Locale)}, + * for example responding to a certain locale change request. + * + * @author Juergen Hoeller + * @author Jean-Pierre Pawlak + * @since 27.02.2003 + * @see #setDefaultLocale + * @see #setLocale + */ +public class CookieLocaleResolver extends CookieGenerator implements LocaleResolver { + + /** + * The name of the request attribute that holds the locale. + *

Only used for overriding a cookie value if the locale has been + * changed in the course of the current request! Use + * {@link org.springframework.web.servlet.support.RequestContext#getLocale} + * to retrieve the current locale in controllers or views. + * @see org.springframework.web.servlet.support.RequestContext#getLocale + */ + public static final String LOCALE_REQUEST_ATTRIBUTE_NAME = CookieLocaleResolver.class.getName() + ".LOCALE"; + + /** + * The default cookie name used if none is explicitly set. + */ + public static final String DEFAULT_COOKIE_NAME = CookieLocaleResolver.class.getName() + ".LOCALE"; + + + private Locale defaultLocale; + + + /** + * Creates a new instance of the {@link CookieLocaleResolver} class + * using the {@link #DEFAULT_COOKIE_NAME default cookie name}. + */ + public CookieLocaleResolver() { + setCookieName(DEFAULT_COOKIE_NAME); + } + + /** + * Set a fixed Locale that this resolver will return if no cookie found. + */ + public void setDefaultLocale(Locale defaultLocale) { + this.defaultLocale = defaultLocale; + } + + /** + * Return the fixed Locale that this resolver will return if no cookie found, + * if any. + */ + protected Locale getDefaultLocale() { + return this.defaultLocale; + } + + + public Locale resolveLocale(HttpServletRequest request) { + // Check request for pre-parsed or preset locale. + Locale locale = (Locale) request.getAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME); + if (locale != null) { + return locale; + } + + // Retrieve and parse cookie value. + Cookie cookie = WebUtils.getCookie(request, getCookieName()); + if (cookie != null) { + locale = StringUtils.parseLocaleString(cookie.getValue()); + if (logger.isDebugEnabled()) { + logger.debug("Parsed cookie value [" + cookie.getValue() + "] into locale '" + locale + "'"); + } + if (locale != null) { + request.setAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME, locale); + return locale; + } + } + + return determineDefaultLocale(request); + } + + public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) { + if (locale != null) { + // Set request attribute and add cookie. + request.setAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME, locale); + addCookie(response, locale.toString()); + } + else { + // Set request attribute to fallback locale and remove cookie. + request.setAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME, determineDefaultLocale(request)); + removeCookie(response); + } + } + + /** + * Determine the default locale for the given request, + * Called if no locale cookie has been found. + *

The default implementation returns the specified default locale, + * if any, else falls back to the request's accept-header locale. + * @param request the request to resolve the locale for + * @return the default locale (never null) + * @see #setDefaultLocale + * @see javax.servlet.http.HttpServletRequest#getLocale() + */ + protected Locale determineDefaultLocale(HttpServletRequest request) { + Locale defaultLocale = getDefaultLocale(); + if (defaultLocale == null) { + defaultLocale = request.getLocale(); + } + return defaultLocale; + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/i18n/FixedLocaleResolver.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/i18n/FixedLocaleResolver.java new file mode 100644 index 00000000000..295a1f779f6 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/i18n/FixedLocaleResolver.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2007 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.web.servlet.i18n; + +import java.util.Locale; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * {@link org.springframework.web.servlet.LocaleResolver} implementation + * that always returns a fixed default locale. Default is the current + * JVM's default locale. + * + *

Note: Does not support setLocale, as the fixed locale + * cannot be changed. + * + * @author Juergen Hoeller + * @since 1.1 + * @see #setDefaultLocale + */ +public class FixedLocaleResolver extends AbstractLocaleResolver { + + /** + * Create a default FixedLocaleResolver, exposing a configured default + * locale (or the JVM's default locale as fallback). + * @see #setDefaultLocale + */ + public FixedLocaleResolver() { + } + + /** + * Create a FixedLocaleResolver that exposes the given locale. + * @param locale the locale to expose + */ + public FixedLocaleResolver(Locale locale) { + setDefaultLocale(locale); + } + + + public Locale resolveLocale(HttpServletRequest request) { + Locale locale = getDefaultLocale(); + if (locale == null) { + locale = Locale.getDefault(); + } + return locale; + } + + public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) { + throw new UnsupportedOperationException( + "Cannot change fixed locale - use a different locale resolution strategy"); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/i18n/LocaleChangeInterceptor.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/i18n/LocaleChangeInterceptor.java new file mode 100644 index 00000000000..0e0ee586dfb --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/i18n/LocaleChangeInterceptor.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2008 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.web.servlet.i18n; + +import java.util.Locale; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.propertyeditors.LocaleEditor; +import org.springframework.web.servlet.LocaleResolver; +import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; +import org.springframework.web.servlet.support.RequestContextUtils; + +/** + * Interceptor that allows for changing the current locale on every request, + * via a configurable request parameter. + * + * @author Juergen Hoeller + * @since 20.06.2003 + * @see org.springframework.web.servlet.LocaleResolver + */ +public class LocaleChangeInterceptor extends HandlerInterceptorAdapter { + + /** + * Default name of the locale specification parameter: "locale". + */ + public static final String DEFAULT_PARAM_NAME = "locale"; + + private String paramName = DEFAULT_PARAM_NAME; + + + /** + * Set the name of the parameter that contains a locale specification + * in a locale change request. Default is "locale". + */ + public void setParamName(String paramName) { + this.paramName = paramName; + } + + /** + * Return the name of the parameter that contains a locale specification + * in a locale change request. + */ + public String getParamName() { + return this.paramName; + } + + + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws ServletException { + + String newLocale = request.getParameter(this.paramName); + if (newLocale != null) { + LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request); + if (localeResolver == null) { + throw new IllegalStateException("No LocaleResolver found: not in a DispatcherServlet request?"); + } + LocaleEditor localeEditor = new LocaleEditor(); + localeEditor.setAsText(newLocale); + localeResolver.setLocale(request, response, (Locale) localeEditor.getValue()); + } + // Proceed in any case. + return true; + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/i18n/SessionLocaleResolver.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/i18n/SessionLocaleResolver.java new file mode 100644 index 00000000000..02127271778 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/i18n/SessionLocaleResolver.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2006 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.web.servlet.i18n; + +import java.util.Locale; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.web.util.WebUtils; + +/** + * Implementation of LocaleResolver that uses a locale attribute in the user's + * session in case of a custom setting, with a fallback to the specified default + * locale or the request's accept-header locale. + * + *

This is most appropriate if the application needs user sessions anyway, + * that is, when the HttpSession does not have to be created for the locale. + * + *

Custom controllers can override the user's locale by calling + * setLocale, e.g. responding to a locale change request. + * + * @author Juergen Hoeller + * @since 27.02.2003 + * @see #setDefaultLocale + * @see #setLocale + */ +public class SessionLocaleResolver extends AbstractLocaleResolver { + + /** + * Name of the session attribute that holds the locale. + * Only used internally by this implementation. + * Use RequestContext(Utils).getLocale() + * to retrieve the current locale in controllers or views. + * @see org.springframework.web.servlet.support.RequestContext#getLocale + * @see org.springframework.web.servlet.support.RequestContextUtils#getLocale + */ + public static final String LOCALE_SESSION_ATTRIBUTE_NAME = SessionLocaleResolver.class.getName() + ".LOCALE"; + + + public Locale resolveLocale(HttpServletRequest request) { + Locale locale = (Locale) WebUtils.getSessionAttribute(request, LOCALE_SESSION_ATTRIBUTE_NAME); + if (locale == null) { + locale = determineDefaultLocale(request); + } + return locale; + } + + /** + * Determine the default locale for the given request, + * Called if no locale session attribute has been found. + *

The default implementation returns the specified default locale, + * if any, else falls back to the request's accept-header locale. + * @param request the request to resolve the locale for + * @return the default locale (never null) + * @see #setDefaultLocale + * @see javax.servlet.http.HttpServletRequest#getLocale() + */ + protected Locale determineDefaultLocale(HttpServletRequest request) { + Locale defaultLocale = getDefaultLocale(); + if (defaultLocale == null) { + defaultLocale = request.getLocale(); + } + return defaultLocale; + } + + public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) { + WebUtils.setSessionAttribute(request, LOCALE_SESSION_ATTRIBUTE_NAME, locale); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/i18n/package.html b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/i18n/package.html new file mode 100644 index 00000000000..ce682d462d0 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/i18n/package.html @@ -0,0 +1,9 @@ + + + +Locale support classes for Spring's web MVC framework. +Provides standard LocaleResolver implementations, +and a HandlerInterceptor for locale changes. + + + diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/AbstractCommandController.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/AbstractCommandController.java new file mode 100644 index 00000000000..d0545af6603 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/AbstractCommandController.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2007 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.web.servlet.mvc; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.validation.BindException; +import org.springframework.web.bind.ServletRequestDataBinder; +import org.springframework.web.servlet.ModelAndView; + +/** + * Abstract base class for custom command controllers. + * + *

Autopopulates a command bean from the request. For command validation, + * a validator (property inherited from {@link BaseCommandController}) can be + * used. + * + *

In most cases this command controller should not be used to handle form + * submission, because functionality for forms is offered in more detail by the + * {@link org.springframework.web.servlet.mvc.AbstractFormController} and its + * corresponding implementations. + * + *

Exposed configuration properties + * (and those defined by superclass):
+ * none (so only those available in superclass).

+ * + *

Workflow + * (and that defined by superclass):
+ * + * @author Rod Johnson + * @author Juergen Hoeller + * @see #setCommandClass + * @see #setCommandName + * @see #setValidator + */ +public abstract class AbstractCommandController extends BaseCommandController { + + /** + * Create a new AbstractCommandController. + */ + public AbstractCommandController() { + } + + /** + * Create a new AbstractCommandController. + * @param commandClass class of the command bean + */ + public AbstractCommandController(Class commandClass) { + setCommandClass(commandClass); + } + + /** + * Create a new AbstractCommandController. + * @param commandClass class of the command bean + * @param commandName name of the command bean + */ + public AbstractCommandController(Class commandClass, String commandName) { + setCommandClass(commandClass); + setCommandName(commandName); + } + + + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) + throws Exception { + + Object command = getCommand(request); + ServletRequestDataBinder binder = bindAndValidate(request, command); + BindException errors = new BindException(binder.getBindingResult()); + return handle(request, response, command, errors); + } + + /** + * Template method for request handling, providing a populated and validated instance + * of the command class, and an Errors object containing binding and validation errors. + *

Call errors.getModel() to populate the ModelAndView model + * with the command and the Errors instance, under the specified command name, + * as expected by the "spring:bind" tag. + * @param request current HTTP request + * @param response current HTTP response + * @param command the populated command object + * @param errors validation errors holder + * @return a ModelAndView to render, or null if handled directly + * @see org.springframework.validation.Errors + * @see org.springframework.validation.BindException#getModel + */ + protected abstract ModelAndView handle( + HttpServletRequest request, HttpServletResponse response, Object command, BindException errors) + throws Exception; + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/AbstractController.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/AbstractController.java new file mode 100644 index 00000000000..676313e7698 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/AbstractController.java @@ -0,0 +1,164 @@ +/* + * Copyright 2002-2007 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.web.servlet.mvc; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.support.WebContentGenerator; +import org.springframework.web.util.WebUtils; + +/** + *

Convenient superclass for controller implementations, using the Template + * Method design pattern.

+ * + *

As stated in the {@link org.springframework.web.servlet.mvc.Controller Controller} + * interface, a lot of functionality is already provided by certain abstract + * base controllers. The AbstractController is one of the most important + * abstract base controller providing basic features such as the generation + * of caching headers and the enabling or disabling of + * supported methods (GET/POST).

+ * + *

Workflow + * (and that defined by interface):
+ *

    + *
  1. {@link #handleRequest(HttpServletRequest,HttpServletResponse) handleRequest()} + * will be called by the DispatcherServlet
  2. + *
  3. Inspection of supported methods (ServletException if request method + * is not support)
  4. + *
  5. If session is required, try to get it (ServletException if not found)
  6. + *
  7. Set caching headers if needed according to cacheSeconds propery
  8. + *
  9. Call abstract method {@link #handleRequestInternal(HttpServletRequest,HttpServletResponse) handleRequestInternal()} + * (optionally synchronizing around the call on the HttpSession), + * which should be implemented by extending classes to provide actual + * functionality to return {@link org.springframework.web.servlet.ModelAndView ModelAndView} objects.
  10. + *
+ *

+ * + *

Exposed configuration properties + * (and those defined by interface):
+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
name + * defaultdescription
supportedMethodsGET,POSTcomma-separated (CSV) list of methods supported by this controller, + * such as GET, POST and PUT
requireSessionfalsewhether a session should be required for requests to be able to + * be handled by this controller. This ensures that derived controller + * can - without fear of null pointers - call request.getSession() to + * retrieve a session. If no session can be found while processing + * the request, a ServletException will be thrown
cacheSeconds-1indicates the amount of seconds to include in the cache header + * for the response following on this request. 0 (zero) will include + * headers for no caching at all, -1 (the default) will not generate + * any headers and any positive number will generate headers + * that state the amount indicated as seconds to cache the content
synchronizeOnSessionfalsewhether the call to handleRequestInternal should be + * synchronized around the HttpSession, to serialize invocations + * from the same client. No effect if there is no HttpSession. + *
+ * + * @author Rod Johnson + * @author Juergen Hoeller + * @see WebContentInterceptor + */ +public abstract class AbstractController extends WebContentGenerator implements Controller { + + private boolean synchronizeOnSession = false; + + + /** + * Set if controller execution should be synchronized on the session, + * to serialize parallel invocations from the same client. + *

More specifically, the execution of the handleRequestInternal + * method will get synchronized if this flag is "true". The best available + * session mutex will be used for the synchronization; ideally, this will + * be a mutex exposed by HttpSessionMutexListener. + *

The session mutex is guaranteed to be the same object during + * the entire lifetime of the session, available under the key defined + * by the SESSION_MUTEX_ATTRIBUTE constant. It serves as a + * safe reference to synchronize on for locking on the current session. + *

In many cases, the HttpSession reference itself is a safe mutex + * as well, since it will always be the same object reference for the + * same active logical session. However, this is not guaranteed across + * different servlet containers; the only 100% safe way is a session mutex. + * @see org.springframework.web.servlet.mvc.AbstractController#handleRequestInternal + * @see org.springframework.web.util.HttpSessionMutexListener + * @see org.springframework.web.util.WebUtils#getSessionMutex(javax.servlet.http.HttpSession) + */ + public final void setSynchronizeOnSession(boolean synchronizeOnSession) { + this.synchronizeOnSession = synchronizeOnSession; + } + + /** + * Return whether controller execution should be synchronized on the session. + */ + public final boolean isSynchronizeOnSession() { + return this.synchronizeOnSession; + } + + + public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) + throws Exception { + + // Delegate to WebContentGenerator for checking and preparing. + checkAndPrepare(request, response, this instanceof LastModified); + + // Execute handleRequestInternal in synchronized block if required. + if (this.synchronizeOnSession) { + HttpSession session = request.getSession(false); + if (session != null) { + Object mutex = WebUtils.getSessionMutex(session); + synchronized (mutex) { + return handleRequestInternal(request, response); + } + } + } + + return handleRequestInternal(request, response); + } + + /** + * Template method. Subclasses must implement this. + * The contract is the same as for handleRequest. + * @see #handleRequest + */ + protected abstract ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) + throws Exception; + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/AbstractFormController.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/AbstractFormController.java new file mode 100644 index 00000000000..3426b181360 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/AbstractFormController.java @@ -0,0 +1,674 @@ +/* + * Copyright 2002-2007 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.web.servlet.mvc; + +import java.util.Map; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.springframework.validation.BindException; +import org.springframework.validation.Errors; +import org.springframework.web.HttpSessionRequiredException; +import org.springframework.web.bind.ServletRequestDataBinder; +import org.springframework.web.servlet.ModelAndView; + +/** + *

Form controller that auto-populates a form bean from the request. + * This, either using a new bean instance per request, or using the same bean + * when the sessionForm property has been set to true.

+ * + *

This class is the base class for both framework subclasses such as + * {@link SimpleFormController} and {@link AbstractWizardFormController} + * and custom form controllers that you may provide yourself.

+ * + *

A form-input view and an after-submission view have to be provided + * programmatically. To provide those views using configuration properties, + * use the {@link SimpleFormController}.

+ * + *

Subclasses need to override showForm to prepare the form view, + * and processFormSubmission to handle submit requests. For the latter, + * binding errors like type mismatches will be reported via the given "errors" holder. + * For additional custom form validation, a validator (property inherited from + * BaseCommandController) can be used, reporting via the same "errors" instance.

+ * + *

Comparing this Controller to the Struts notion of the Action + * shows us that with Spring, you can use any ordinary JavaBeans or database- + * backed JavaBeans without having to implement a framework-specific class + * (like Struts' ActionForm). More complex properties of JavaBeans + * (Dates, Locales, but also your own application-specific or compound types) + * can be represented and submitted to the controller, by using the notion of + * a java.beans.PropertyEditor. For more information on that + * subject, see the workflow of this controller and the explanation of the + * {@link BaseCommandController}.

+ * + *

Workflow + * (and that defined by superclass):
+ *

    + *
  1. The controller receives a request for a new form (typically a GET).
  2. + *
  3. Call to {@link #formBackingObject formBackingObject()} which by default, + * returns an instance of the commandClass that has been configured + * (see the properties the superclass exposes), but can also be overridden + * to e.g. retrieve an object from the database (that needs to be modified + * using the form).
  4. + *
  5. Call to {@link #initBinder initBinder()} which allows you to register + * custom editors for certain fields (often properties of non-primitive + * or non-String types) of the command class. This will render appropriate + * Strings for those property values, e.g. locale-specific date strings.
  6. + *
  7. Only if bindOnNewForm is set to true, then + * {@link org.springframework.web.bind.ServletRequestDataBinder ServletRequestDataBinder} + * gets applied to populate the new form object with initial request parameters and the + * {@link #onBindOnNewForm(HttpServletRequest, Object, BindException)} callback method is + * called. Note: any defined Validators are not applied at this point, to allow + * partial binding. However be aware that any Binder customizations applied via + * initBinder() (such as + * {@link org.springframework.validation.DataBinder#setRequiredFields(String[])} will + * still apply. As such, if using bindOnNewForm=true and initBinder() customizations are + * used to validate fields instead of using Validators, in the case that only some fields + * will be populated for the new form, there will potentially be some bind errors for + * missing fields in the errors object. Any view (JSP, etc.) that displays binder errors + * needs to be intelligent and for this case take into account whether it is displaying the + * initial form view or subsequent post results, skipping error display for the former.
  8. + *
  9. Call to {@link #showForm(HttpServletRequest, HttpServletResponse, BindException) showForm()} + * to return a View that should be rendered (typically the view that renders + * the form). This method has to be implemented in subclasses.
  10. + *
  11. The showForm() implementation will call {@link #referenceData referenceData()}, + * which you can implement to provide any relevant reference data you might need + * when editing a form (e.g. a List of Locale objects you're going to let the + * user select one from).
  12. + *
  13. Model gets exposed and view gets rendered, to let the user fill in the form.
  14. + *
  15. The controller receives a form submission (typically a POST). + * To use a different way of detecting a form submission, override the + * {@link #isFormSubmission isFormSubmission} method. + *
  16. + *
  17. If sessionForm is not set, {@link #formBackingObject formBackingObject()} + * is called to retrieve a form object. Otherwise, the controller tries to + * find the command object which is already bound in the session. If it cannot + * find the object, it does a call to {@link #handleInvalidSubmit handleInvalidSubmit} + * which - by default - tries to create a new form object and resubmit the form.
  18. + *
  19. The {@link org.springframework.web.bind.ServletRequestDataBinder ServletRequestDataBinder} + * gets applied to populate the form object with current request parameters. + *
  20. Call to {@link #onBind onBind(HttpServletRequest, Object, Errors)} which allows + * you to do custom processing after binding but before validation (e.g. to manually + * bind request parameters to bean properties, to be seen by the Validator).
  21. + *
  22. If validateOnBinding is set, a registered Validator will be invoked. + * The Validator will check the form object properties, and register corresponding + * errors via the given {@link org.springframework.validation.Errors Errors}
  23. object. + *
  24. Call to {@link #onBindAndValidate onBindAndValidate()} which allows you + * to do custom processing after binding and validation (e.g. to manually + * bind request parameters, and to validate them outside a Validator).
  25. + *
  26. Call {@link #processFormSubmission(HttpServletRequest, HttpServletResponse, + * Object, BindException) processFormSubmission()} to process the submission, with + * or without binding errors. This method has to be implemented in subclasses.
  27. + *
+ *

+ * + *

In session form mode, a submission without an existing form object in the + * session is considered invalid, like in case of a resubmit/reload by the browser. + * The {@link #handleInvalidSubmit handleInvalidSubmit} method is invoked then, + * by default trying to resubmit. It can be overridden in subclasses to show + * corresponding messages or to redirect to a new form, in order to avoid duplicate + * submissions. The form object in the session can be considered a transaction + * token in that case.

+ * + *

Note that views should never retrieve form beans from the session but always + * from the request, as prepared by the form controller. Remember that some view + * technologies like Velocity cannot even access a HTTP session.

+ * + *

Exposed configuration properties + * (and those defined by superclass):
+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
namedefaultdescription
bindOnNewFormfalseIndicates whether to bind servlet request parameters when + * creating a new form. Otherwise, the parameters will only be + * bound on form submission attempts.
sessionFormfalseIndicates whether the form object should be kept in the session + * when a user asks for a new form. This allows you e.g. to retrieve + * an object from the database, let the user edit it, and then persist + * it again. Otherwise, a new command object will be created for each + * request (even when showing the form again after validation errors).
+ *

+ * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Alef Arendsen + * @author Rob Harrop + * @author Colin Sampaleanu + * @see #showForm(HttpServletRequest, HttpServletResponse, BindException) + * @see #processFormSubmission + * @see SimpleFormController + * @see AbstractWizardFormController + */ +public abstract class AbstractFormController extends BaseCommandController { + + private boolean bindOnNewForm = false; + + private boolean sessionForm = false; + + + /** + * Create a new AbstractFormController. + *

Subclasses should set the following properties, either in the constructor + * or via a BeanFactory: commandName, commandClass, bindOnNewForm, sessionForm. + * Note that "commandClass" doesn't need to be set when overriding + * {@link #formBackingObject}, since the latter determines the class anyway. + *

"cacheSeconds" is by default set to 0 (-> no caching for all form controllers). + * @see #setCommandName + * @see #setCommandClass + * @see #setBindOnNewForm + * @see #setSessionForm + * @see #formBackingObject + */ + public AbstractFormController() { + setCacheSeconds(0); + } + + /** + * Set whether request parameters should be bound to the form object + * in case of a non-submitting request, that is, a new form. + */ + public final void setBindOnNewForm(boolean bindOnNewForm) { + this.bindOnNewForm = bindOnNewForm; + } + + /** + * Return true if request parameters should be bound in case of a new form. + */ + public final boolean isBindOnNewForm() { + return this.bindOnNewForm; + } + + /** + * Activate/deactivate session form mode. In session form mode, + * the form is stored in the session to keep the form object instance + * between requests, instead of creating a new one on each request. + *

This is necessary for either wizard-style controllers that populate a + * single form object from multiple pages, or forms that populate a persistent + * object that needs to be identical to allow for tracking changes. + *

Please note that the {@link AbstractFormController} class (and all + * subclasses of it unless stated to the contrary) do not support + * the notion of a conversation. This is important in the context of this + * property, because it means that there is only one form per session: + * this means that if session form mode is activated and a user opens up + * say two tabs in their browser and attempts to edit two distinct objects + * using the same form, then the shared session state can potentially + * (and most probably will) be overwritten by the last tab to be opened, + * which can lead to errors when either of the forms in each is finally + * submitted. + *

If you need to have per-form, per-session state management (that is, + * stateful web conversations), the recommendation is to use + * Spring WebFlow, + * which has full support for conversations and has a much more flexible + * usage model overall. + * @param sessionForm true if session form mode is to be activated + */ + public final void setSessionForm(boolean sessionForm) { + this.sessionForm = sessionForm; + } + + /** + * Return true if session form mode is activated. + */ + public final boolean isSessionForm() { + return this.sessionForm; + } + + + /** + * Handles two cases: form submissions and showing a new form. + * Delegates the decision between the two to {@link #isFormSubmission}, + * always treating requests without existing form session attribute + * as new form when using session form mode. + * @see #isFormSubmission + * @see #showNewForm + * @see #processFormSubmission + */ + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) + throws Exception { + + // Form submission or new form to show? + if (isFormSubmission(request)) { + // Fetch form object from HTTP session, bind, validate, process submission. + try { + Object command = getCommand(request); + ServletRequestDataBinder binder = bindAndValidate(request, command); + BindException errors = new BindException(binder.getBindingResult()); + return processFormSubmission(request, response, command, errors); + } + catch (HttpSessionRequiredException ex) { + // Cannot submit a session form if no form object is in the session. + if (logger.isDebugEnabled()) { + logger.debug("Invalid submit detected: " + ex.getMessage()); + } + return handleInvalidSubmit(request, response); + } + } + + else { + // New form to show: render form view. + return showNewForm(request, response); + } + } + + /** + * Determine if the given request represents a form submission. + *

The default implementation treats a POST request as form submission. + * Note: If the form session attribute doesn't exist when using session form + * mode, the request is always treated as new form by handleRequestInternal. + *

Subclasses can override this to use a custom strategy, e.g. a specific + * request parameter (assumably a hidden field or submit button name). + * @param request current HTTP request + * @return if the request represents a form submission + */ + protected boolean isFormSubmission(HttpServletRequest request) { + return "POST".equals(request.getMethod()); + } + + /** + * Return the name of the HttpSession attribute that holds the form object + * for this form controller. + *

The default implementation delegates to the {@link #getFormSessionAttributeName()} + * variant without arguments. + * @param request current HTTP request + * @return the name of the form session attribute, or null if not in session form mode + * @see #getFormSessionAttributeName + * @see javax.servlet.http.HttpSession#getAttribute + */ + protected String getFormSessionAttributeName(HttpServletRequest request) { + return getFormSessionAttributeName(); + } + + /** + * Return the name of the HttpSession attribute that holds the form object + * for this form controller. + *

Default is an internal name, of no relevance to applications, as the form + * session attribute is not usually accessed directly. Can be overridden to use + * an application-specific attribute name, which allows other code to access + * the session attribute directly. + * @return the name of the form session attribute + * @see javax.servlet.http.HttpSession#getAttribute + */ + protected String getFormSessionAttributeName() { + return getClass().getName() + ".FORM." + getCommandName(); + } + + + /** + * Show a new form. Prepares a backing object for the current form + * and the given request, including checking its validity. + * @param request current HTTP request + * @param response current HTTP response + * @return the prepared form view + * @throws Exception in case of an invalid new form object + * @see #getErrorsForNewForm + */ + protected final ModelAndView showNewForm(HttpServletRequest request, HttpServletResponse response) + throws Exception { + + logger.debug("Displaying new form"); + return showForm(request, response, getErrorsForNewForm(request)); + } + + /** + * Create a BindException instance for a new form. + * Called by {@link #showNewForm}. + *

Can be used directly when intending to show a new form but with + * special errors registered on it (for example, on invalid submit). + * Usually, the resulting BindException will be passed to + * {@link #showForm(HttpServletRequest, HttpServletResponse, BindException)}, + * after registering the errors on it. + * @param request current HTTP request + * @return the BindException instance + * @throws Exception in case of an invalid new form object + * @see #showNewForm + * @see #showForm(HttpServletRequest, HttpServletResponse, BindException) + * @see #handleInvalidSubmit + */ + protected final BindException getErrorsForNewForm(HttpServletRequest request) throws Exception { + // Create form-backing object for new form. + Object command = formBackingObject(request); + if (command == null) { + throw new ServletException("Form object returned by formBackingObject() must not be null"); + } + if (!checkCommand(command)) { + throw new ServletException("Form object returned by formBackingObject() must match commandClass"); + } + + // Bind without validation, to allow for prepopulating a form, and for + // convenient error evaluation in views (on both first attempt and resubmit). + ServletRequestDataBinder binder = createBinder(request, command); + BindException errors = new BindException(binder.getBindingResult()); + if (isBindOnNewForm()) { + logger.debug("Binding to new form"); + binder.bind(request); + onBindOnNewForm(request, command, errors); + } + + // Return BindException object that resulted from binding. + return errors; + } + + /** + * Callback for custom post-processing in terms of binding for a new form. + * Called when preparing a new form if bindOnNewForm is true. + *

The default implementation delegates to onBindOnNewForm(request, command). + * @param request current HTTP request + * @param command the command object to perform further binding on + * @param errors validation errors holder, allowing for additional + * custom registration of binding errors + * @throws Exception in case of invalid state or arguments + * @see #onBindOnNewForm(javax.servlet.http.HttpServletRequest, Object) + * @see #setBindOnNewForm + */ + protected void onBindOnNewForm(HttpServletRequest request, Object command, BindException errors) + throws Exception { + + onBindOnNewForm(request, command); + } + + /** + * Callback for custom post-processing in terms of binding for a new form. + *

Called by the default implementation of the + * {@link #onBindOnNewForm(HttpServletRequest, Object, BindException)} variant + * with all parameters, after standard binding when displaying the form view. + * Only called if bindOnNewForm is set to true. + *

The default implementation is empty. + * @param request current HTTP request + * @param command the command object to perform further binding on + * @throws Exception in case of invalid state or arguments + * @see #onBindOnNewForm(HttpServletRequest, Object, BindException) + * @see #setBindOnNewForm(boolean) + */ + protected void onBindOnNewForm(HttpServletRequest request, Object command) throws Exception { + } + + + /** + * Return the form object for the given request. + *

Calls {@link #formBackingObject} if not in session form mode. + * Else, retrieves the form object from the session. Note that the form object + * gets removed from the session, but it will be re-added when showing the + * form for resubmission. + * @param request current HTTP request + * @return object form to bind onto + * @throws org.springframework.web.HttpSessionRequiredException + * if a session was expected but no active session (or session form object) found + * @throws Exception in case of invalid state or arguments + * @see #formBackingObject + */ + protected final Object getCommand(HttpServletRequest request) throws Exception { + // If not in session-form mode, create a new form-backing object. + if (!isSessionForm()) { + return formBackingObject(request); + } + + // Session-form mode: retrieve form object from HTTP session attribute. + HttpSession session = request.getSession(false); + if (session == null) { + throw new HttpSessionRequiredException("Must have session when trying to bind (in session-form mode)"); + } + String formAttrName = getFormSessionAttributeName(request); + Object sessionFormObject = session.getAttribute(formAttrName); + if (sessionFormObject == null) { + throw new HttpSessionRequiredException("Form object not found in session (in session-form mode)"); + } + + // Remove form object from HTTP session: we might finish the form workflow + // in this request. If it turns out that we need to show the form view again, + // we'll re-bind the form object to the HTTP session. + if (logger.isDebugEnabled()) { + logger.debug("Removing form session attribute [" + formAttrName + "]"); + } + session.removeAttribute(formAttrName); + + return currentFormObject(request, sessionFormObject); + } + + /** + * Retrieve a backing object for the current form from the given request. + *

The properties of the form object will correspond to the form field values + * in your form view. This object will be exposed in the model under the specified + * command name, to be accessed under that name in the view: for example, with + * a "spring:bind" tag. The default command name is "command". + *

Note that you need to activate session form mode to reuse the form-backing + * object across the entire form workflow. Else, a new instance of the command + * class will be created for each submission attempt, just using this backing + * object as template for the initial form. + *

The default implementation calls {@link #createCommand()}, + * creating a new empty instance of the specified command class. + * Subclasses can override this to provide a preinitialized backing object. + * @param request current HTTP request + * @return the backing object + * @throws Exception in case of invalid state or arguments + * @see #setCommandName + * @see #setCommandClass + * @see #createCommand + */ + protected Object formBackingObject(HttpServletRequest request) throws Exception { + return createCommand(); + } + + /** + * Return the current form object to use for binding and further processing, + * based on the passed-in form object as found in the HttpSession. + *

The default implementation simply returns the session form object as-is. + * Subclasses can override this to post-process the session form object, + * for example reattaching it to a persistence manager. + * @param sessionFormObject the form object retrieved from the HttpSession + * @return the form object to use for binding and further processing + * @throws Exception in case of invalid state or arguments + */ + protected Object currentFormObject(HttpServletRequest request, Object sessionFormObject) throws Exception { + return sessionFormObject; + } + + + /** + * Prepare the form model and view, including reference and error data. + * Can show a configured form page, or generate a form view programmatically. + *

A typical implementation will call + * showForm(request, errors, "myView") + * to prepare the form view for a specific view name, returning the + * ModelAndView provided there. + *

For building a custom ModelAndView, call errors.getModel() + * to populate the ModelAndView model with the command and the Errors instance, + * under the specified command name, as expected by the "spring:bind" tag. + * You also need to include the model returned by {@link #referenceData}. + *

Note: If you decide to have a "formView" property specifying the + * view name, consider using SimpleFormController. + * @param request current HTTP request + * @param response current HTTP response + * @param errors validation errors holder + * @return the prepared form view, or null if handled directly + * @throws Exception in case of invalid state or arguments + * @see #showForm(HttpServletRequest, BindException, String) + * @see org.springframework.validation.Errors + * @see org.springframework.validation.BindException#getModel + * @see #referenceData(HttpServletRequest, Object, Errors) + * @see SimpleFormController#setFormView + */ + protected abstract ModelAndView showForm( + HttpServletRequest request, HttpServletResponse response, BindException errors) + throws Exception; + + /** + * Prepare model and view for the given form, including reference and errors. + *

In session form mode: Re-puts the form object in the session when + * returning to the form, as it has been removed by getCommand. + *

Can be used in subclasses to redirect back to a specific form page. + * @param request current HTTP request + * @param errors validation errors holder + * @param viewName name of the form view + * @return the prepared form view + * @throws Exception in case of invalid state or arguments + */ + protected final ModelAndView showForm(HttpServletRequest request, BindException errors, String viewName) + throws Exception { + + return showForm(request, errors, viewName, null); + } + + /** + * Prepare model and view for the given form, including reference and errors, + * adding a controller-specific control model. + *

In session form mode: Re-puts the form object in the session when returning + * to the form, as it has been removed by getCommand. + *

Can be used in subclasses to redirect back to a specific form page. + * @param request current HTTP request + * @param errors validation errors holder + * @param viewName name of the form view + * @param controlModel model map containing controller-specific control data + * (e.g. current page in wizard-style controllers or special error message) + * @return the prepared form view + * @throws Exception in case of invalid state or arguments + */ + protected final ModelAndView showForm( + HttpServletRequest request, BindException errors, String viewName, Map controlModel) + throws Exception { + + // In session form mode, re-expose form object as HTTP session attribute. + // Re-binding is necessary for proper state handling in a cluster, + // to notify other nodes of changes in the form object. + if (isSessionForm()) { + String formAttrName = getFormSessionAttributeName(request); + if (logger.isDebugEnabled()) { + logger.debug("Setting form session attribute [" + formAttrName + "] to: " + errors.getTarget()); + } + request.getSession().setAttribute(formAttrName, errors.getTarget()); + } + + // Fetch errors model as starting point, containing form object under + // "commandName", and corresponding Errors instance under internal key. + Map model = errors.getModel(); + + // Merge reference data into model, if any. + Map referenceData = referenceData(request, errors.getTarget(), errors); + if (referenceData != null) { + model.putAll(referenceData); + } + + // Merge control attributes into model, if any. + if (controlModel != null) { + model.putAll(controlModel); + } + + // Trigger rendering of the specified view, using the final model. + return new ModelAndView(viewName, model); + } + + /** + * Create a reference data map for the given request, consisting of + * bean name/bean instance pairs as expected by ModelAndView. + *

The default implementation returns null. + * Subclasses can override this to set reference data used in the view. + * @param request current HTTP request + * @param command form object with request parameters bound onto it + * @param errors validation errors holder + * @return a Map with reference data entries, or null if none + * @throws Exception in case of invalid state or arguments + * @see ModelAndView + */ + protected Map referenceData(HttpServletRequest request, Object command, Errors errors) throws Exception { + return null; + } + + + /** + * Process form submission request. Called by {@link #handleRequestInternal} + * in case of a form submission, with or without binding errors. Implementations + * need to proceed properly, typically showing a form view in case of binding + * errors or performing a submit action else. + *

Subclasses can implement this to provide custom submission handling like + * triggering a custom action. They can also provide custom validation and call + * {@link #showForm(HttpServletRequest, HttpServletResponse, BindException)} + * or proceed with the submission accordingly. + *

For a success view, call errors.getModel() to populate the + * ModelAndView model with the command and the Errors instance, under the + * specified command name, as expected by the "spring:bind" tag. For a form view, + * simply return the ModelAndView object provided by + * {@link #showForm(HttpServletRequest, HttpServletResponse, BindException)}. + * @param request current servlet request + * @param response current servlet response + * @param command form object with request parameters bound onto it + * @param errors holder without errors (subclass can add errors if it wants to) + * @return the prepared model and view, or null + * @throws Exception in case of errors + * @see #handleRequestInternal + * @see #isFormSubmission + * @see #showForm(HttpServletRequest, HttpServletResponse, BindException) + * @see org.springframework.validation.Errors + * @see org.springframework.validation.BindException#getModel + */ + protected abstract ModelAndView processFormSubmission( + HttpServletRequest request, HttpServletResponse response, Object command, BindException errors) + throws Exception; + + /** + * Handle an invalid submit request, e.g. when in session form mode but no form object + * was found in the session (like in case of an invalid resubmit by the browser). + *

The default implementation simply tries to resubmit the form with a new + * form object. This should also work if the user hit the back button, changed + * some form data, and resubmitted the form. + *

Note: To avoid duplicate submissions, you need to override this method. + * Either show some "invalid submit" message, or call {@link #showNewForm} for + * resetting the form (prepopulating it with the current values if "bindOnNewForm" + * is true). In this case, the form object in the session serves as transaction token. + *

+	 * protected ModelAndView handleInvalidSubmit(HttpServletRequest request, HttpServletResponse response) throws Exception {
+	 *   return showNewForm(request, response);
+	 * }
+ * You can also show a new form but with special errors registered on it: + *
+	 * protected ModelAndView handleInvalidSubmit(HttpServletRequest request, HttpServletResponse response) throws Exception {
+	 *   BindException errors = getErrorsForNewForm(request);
+	 *   errors.reject("duplicateFormSubmission", "Duplicate form submission");
+	 *   return showForm(request, response, errors);
+	 * }
+ * @param request current HTTP request + * @param response current HTTP response + * @return a prepared view, or null if handled directly + * @throws Exception in case of errors + * @see #showNewForm + * @see #getErrorsForNewForm + * @see #showForm(HttpServletRequest, HttpServletResponse, BindException) + * @see #setBindOnNewForm + */ + protected ModelAndView handleInvalidSubmit(HttpServletRequest request, HttpServletResponse response) + throws Exception { + + Object command = formBackingObject(request); + ServletRequestDataBinder binder = bindAndValidate(request, command); + BindException errors = new BindException(binder.getBindingResult()); + return processFormSubmission(request, response, command, errors); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/AbstractUrlViewController.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/AbstractUrlViewController.java new file mode 100644 index 00000000000..fec8666a644 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/AbstractUrlViewController.java @@ -0,0 +1,111 @@ +/* + * Copyright 2002-2007 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.web.servlet.mvc; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.util.Assert; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.util.UrlPathHelper; + +/** + * Abstract base class for Controllers that return a view name + * based on the request URL. + * + *

Provides infrastructure for determining view names from URLs and configurable + * URL lookup. For information on the latter, see alwaysUseFullPath + * and urlDecode properties. + * + * @author Juergen Hoeller + * @since 1.2.6 + * @see #setAlwaysUseFullPath + * @see #setUrlDecode + */ +public abstract class AbstractUrlViewController extends AbstractController { + + private UrlPathHelper urlPathHelper = new UrlPathHelper(); + + + /** + * Set if URL lookup should always use full path within current servlet + * context. Else, the path within the current servlet mapping is used + * if applicable (i.e. in the case of a ".../*" servlet mapping in web.xml). + * Default is "false". + * @see org.springframework.web.util.UrlPathHelper#setAlwaysUseFullPath + */ + public void setAlwaysUseFullPath(boolean alwaysUseFullPath) { + this.urlPathHelper.setAlwaysUseFullPath(alwaysUseFullPath); + } + + /** + * Set if context path and request URI should be URL-decoded. + * Both are returned undecoded by the Servlet API, + * in contrast to the servlet path. + *

Uses either the request encoding or the default encoding according + * to the Servlet spec (ISO-8859-1). + * @see org.springframework.web.util.UrlPathHelper#setUrlDecode + */ + public void setUrlDecode(boolean urlDecode) { + this.urlPathHelper.setUrlDecode(urlDecode); + } + + /** + * Set the UrlPathHelper to use for the resolution of lookup paths. + *

Use this to override the default UrlPathHelper with a custom subclass, + * or to share common UrlPathHelper settings across multiple MethodNameResolvers + * and HandlerMappings. + * @see org.springframework.web.servlet.handler.AbstractUrlHandlerMapping#setUrlPathHelper + */ + public void setUrlPathHelper(UrlPathHelper urlPathHelper) { + Assert.notNull(urlPathHelper, "UrlPathHelper must not be null"); + this.urlPathHelper = urlPathHelper; + } + + /** + * Return the UrlPathHelper to use for the resolution of lookup paths. + */ + protected UrlPathHelper getUrlPathHelper() { + return this.urlPathHelper; + } + + + /** + * Retrieves the URL path to use for lookup and delegates to + * {@link #getViewNameForRequest}. + */ + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) { + String lookupPath = getUrlPathHelper().getLookupPathForRequest(request); + String viewName = getViewNameForRequest(request); + if (logger.isDebugEnabled()) { + logger.debug("Returning view name '" + viewName + "' for lookup path [" + lookupPath + "]"); + } + return new ModelAndView(viewName); + } + + /** + * Return the name of the view to render for this request, based on the + * given lookup path. Called by {@link #handleRequestInternal}. + * @param request current HTTP request + * @return a view name for this request (never null) + * @see #handleRequestInternal + * @see #setAlwaysUseFullPath + * @see #setUrlDecode + */ + protected abstract String getViewNameForRequest(HttpServletRequest request); + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/AbstractWizardFormController.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/AbstractWizardFormController.java new file mode 100644 index 00000000000..7f40415904d --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/AbstractWizardFormController.java @@ -0,0 +1,743 @@ +/* + * Copyright 2002-2007 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.web.servlet.mvc; + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.validation.BindException; +import org.springframework.validation.Errors; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.util.WebUtils; + +/** + * Form controller for typical wizard-style workflows. + * + *

In contrast to classic forms, wizards have more than one form view page. + * Therefore, there are various actions instead of one single submit action: + *

+ * + *

Finish and cancel actions can be triggered by request parameters, named + * PARAM_FINISH ("_finish") and PARAM_CANCEL ("_cancel"), ignoring parameter + * values to allow for HTML buttons. The target page for page changes can be + * specified by PARAM_TARGET, appending the page number to the parameter name + * (e.g. "_target1"). The action parameters are recognized when triggered by + * image buttons too (via "_finish.x", "_abort.x", or "_target1.x"). + * + *

The current page number will be stored in the session. It can also be + * specified as request parameter PARAM_PAGE ("_page") in order to properly handle + * usage of the back button in a browser: In this case, a submission will always + * contain the correct page number, even if the user submitted from an old view. + * + *

The page can only be changed if it validates correctly, except if a + * "dirty back" or "dirty forward" is allowed. At finish, all pages get + * validated again to guarantee a consistent state. + * + *

Note that a validator's default validate method is not executed when using + * this class! Rather, the {@link #validatePage} implementation should call + * special validateXXX methods that the validator needs to provide, + * validating certain pieces of the object. These can be combined to validate + * the elements of individual pages. + * + *

Note: Page numbering starts with 0, to be able to pass an array + * consisting of the corresponding view names to the "pages" bean property. + * + * @author Juergen Hoeller + * @since 25.04.2003 + * @see #setPages + * @see #validatePage + * @see #processFinish + * @see #processCancel + */ +public abstract class AbstractWizardFormController extends AbstractFormController { + + /** + * Parameter triggering the finish action. + * Can be called from any wizard page! + */ + public static final String PARAM_FINISH = "_finish"; + + /** + * Parameter triggering the cancel action. + * Can be called from any wizard page! + */ + public static final String PARAM_CANCEL = "_cancel"; + + /** + * Parameter specifying the target page, + * appending the page number to the name. + */ + public static final String PARAM_TARGET = "_target"; + + /** + * Parameter specifying the current page as value. Not necessary on + * form pages, but allows to properly handle usage of the back button. + * @see #setPageAttribute + */ + public static final String PARAM_PAGE = "_page"; + + + private String[] pages; + + private String pageAttribute; + + private boolean allowDirtyBack = true; + + private boolean allowDirtyForward = false; + + + /** + * Create a new AbstractWizardFormController. + *

"sessionForm" is automatically turned on, "validateOnBinding" + * turned off, and "cacheSeconds" set to 0 by the base class + * (-> no caching for all form controllers). + */ + public AbstractWizardFormController() { + // AbstractFormController sets default cache seconds to 0. + super(); + + // Always needs session to keep data from all pages. + setSessionForm(true); + + // Never validate everything on binding -> + // wizards validate individual pages. + setValidateOnBinding(false); + } + + /** + * Set the wizard pages, i.e. the view names for the pages. + * The array index is interpreted as page number. + * @param pages view names for the pages + */ + public final void setPages(String[] pages) { + if (pages == null || pages.length == 0) { + throw new IllegalArgumentException("No wizard pages defined"); + } + this.pages = pages; + } + + /** + * Return the wizard pages, i.e. the view names for the pages. + * The array index corresponds to the page number. + *

Note that a concrete wizard form controller might override + * {@link #getViewName(HttpServletRequest, Object, int)} to + * determine the view name for each page dynamically. + * @see #getViewName(javax.servlet.http.HttpServletRequest, Object, int) + */ + public final String[] getPages() { + return this.pages; + } + + /** + * Return the number of wizard pages. + * Useful to check whether the last page has been reached. + *

Note that a concrete wizard form controller might override + * {@link #getPageCount(HttpServletRequest, Object)} to determine + * the page count dynamically. The default implementation of that extended + * getPageCount variant returns the static page count as + * determined by this getPageCount() method. + * @see #getPageCount(javax.servlet.http.HttpServletRequest, Object) + */ + protected final int getPageCount() { + return this.pages.length; + } + + /** + * Set the name of the page attribute in the model, containing + * an Integer with the current page number. + *

This will be necessary for single views rendering multiple view pages. + * It also allows for specifying the optional "_page" parameter. + * @param pageAttribute name of the page attribute + * @see #PARAM_PAGE + */ + public final void setPageAttribute(String pageAttribute) { + this.pageAttribute = pageAttribute; + } + + /** + * Return the name of the page attribute in the model. + */ + public final String getPageAttribute() { + return this.pageAttribute; + } + + /** + * Set if "dirty back" is allowed, that is, if moving to a former wizard + * page is allowed in case of validation errors for the current page. + * @param allowDirtyBack if "dirty back" is allowed + */ + public final void setAllowDirtyBack(boolean allowDirtyBack) { + this.allowDirtyBack = allowDirtyBack; + } + + /** + * Return whether "dirty back" is allowed. + */ + public final boolean isAllowDirtyBack() { + return this.allowDirtyBack; + } + + /** + * Set if "dirty forward" is allowed, that is, if moving to a later wizard + * page is allowed in case of validation errors for the current page. + * @param allowDirtyForward if "dirty forward" is allowed + */ + public final void setAllowDirtyForward(boolean allowDirtyForward) { + this.allowDirtyForward = allowDirtyForward; + } + + /** + * Return whether "dirty forward" is allowed. + */ + public final boolean isAllowDirtyForward() { + return this.allowDirtyForward; + } + + + /** + * Calls page-specific onBindAndValidate method. + */ + protected final void onBindAndValidate(HttpServletRequest request, Object command, BindException errors) + throws Exception { + + onBindAndValidate(request, command, errors, getCurrentPage(request)); + } + + /** + * Callback for custom post-processing in terms of binding and validation. + * Called on each submit, after standard binding but before page-specific + * validation of this wizard form controller. + *

Note: AbstractWizardFormController does not perform standand + * validation on binding but rather applies page-specific validation + * on processing the form submission. + * @param request current HTTP request + * @param command bound command + * @param errors Errors instance for additional custom validation + * @param page current wizard page + * @throws Exception in case of invalid state or arguments + * @see #bindAndValidate + * @see #processFormSubmission + * @see org.springframework.validation.Errors + */ + protected void onBindAndValidate(HttpServletRequest request, Object command, BindException errors, int page) + throws Exception { + } + + /** + * Consider an explicit finish or cancel request as a form submission too. + * @see #isFinishRequest(javax.servlet.http.HttpServletRequest) + * @see #isCancelRequest(javax.servlet.http.HttpServletRequest) + */ + protected boolean isFormSubmission(HttpServletRequest request) { + return super.isFormSubmission(request) || isFinishRequest(request) || isCancelRequest(request); + } + + /** + * Calls page-specific referenceData method. + */ + protected final Map referenceData(HttpServletRequest request, Object command, Errors errors) + throws Exception { + + return referenceData(request, command, errors, getCurrentPage(request)); + } + + /** + * Create a reference data map for the given request, consisting of + * bean name/bean instance pairs as expected by ModelAndView. + *

The default implementation delegates to referenceData(HttpServletRequest, int). + * Subclasses can override this to set reference data used in the view. + * @param request current HTTP request + * @param command form object with request parameters bound onto it + * @param errors validation errors holder + * @param page current wizard page + * @return a Map with reference data entries, or null if none + * @throws Exception in case of invalid state or arguments + * @see #referenceData(HttpServletRequest, int) + * @see ModelAndView + */ + protected Map referenceData(HttpServletRequest request, Object command, Errors errors, int page) + throws Exception { + + return referenceData(request, page); + } + + /** + * Create a reference data map for the given request, consisting of + * bean name/bean instance pairs as expected by ModelAndView. + *

The default implementation returns null. + * Subclasses can override this to set reference data used in the view. + * @param request current HTTP request + * @param page current wizard page + * @return a Map with reference data entries, or null if none + * @throws Exception in case of invalid state or arguments + * @see ModelAndView + */ + protected Map referenceData(HttpServletRequest request, int page) throws Exception { + return null; + } + + + /** + * Show the first page as form view. + *

This can be overridden in subclasses, e.g. to prepare wizard-specific + * error views in case of an Exception. + */ + protected ModelAndView showForm( + HttpServletRequest request, HttpServletResponse response, BindException errors) + throws Exception { + + return showPage(request, errors, getInitialPage(request, errors.getTarget())); + } + + /** + * Prepare the form model and view, including reference and error data, + * for the given page. Can be used in {@link #processFinish} implementations, + * to show the corresponding page in case of validation errors. + * @param request current HTTP request + * @param errors validation errors holder + * @param page number of page to show + * @return the prepared form view + * @throws Exception in case of invalid state or arguments + */ + protected final ModelAndView showPage(HttpServletRequest request, BindException errors, int page) + throws Exception { + + if (page >= 0 && page < getPageCount(request, errors.getTarget())) { + if (logger.isDebugEnabled()) { + logger.debug("Showing wizard page " + page + " for form bean '" + getCommandName() + "'"); + } + + // Set page session attribute, expose overriding request attribute. + Integer pageInteger = new Integer(page); + String pageAttrName = getPageSessionAttributeName(request); + if (isSessionForm()) { + if (logger.isDebugEnabled()) { + logger.debug("Setting page session attribute [" + pageAttrName + "] to: " + pageInteger); + } + request.getSession().setAttribute(pageAttrName, pageInteger); + } + request.setAttribute(pageAttrName, pageInteger); + + // Set page request attribute for evaluation by views. + Map controlModel = new HashMap(); + if (this.pageAttribute != null) { + controlModel.put(this.pageAttribute, new Integer(page)); + } + String viewName = getViewName(request, errors.getTarget(), page); + return showForm(request, errors, viewName, controlModel); + } + + else { + throw new ServletException("Invalid wizard page number: " + page); + } + } + + /** + * Return the page count for this wizard form controller. + * The default implementation delegates to {@link #getPageCount()}. + *

Can be overridden to dynamically adapt the page count. + * @param request current HTTP request + * @param command the command object as returned by formBackingObject + * @return the current page count + * @see #getPageCount + */ + protected int getPageCount(HttpServletRequest request, Object command) { + return getPageCount(); + } + + /** + * Return the name of the view for the specified page of this wizard form controller. + *

The default implementation takes the view name from the {@link #getPages()} array. + *

Can be overridden to dynamically switch the page view or to return view names + * for dynamically defined pages. + * @param request current HTTP request + * @param command the command object as returned by formBackingObject + * @param page the current page number + * @return the current page count + * @see #getPageCount + */ + protected String getViewName(HttpServletRequest request, Object command, int page) { + return getPages()[page]; + } + + /** + * Return the initial page of the wizard, that is, the page shown at wizard startup. + *

The default implementation delegates to {@link #getInitialPage(HttpServletRequest)}. + * @param request current HTTP request + * @param command the command object as returned by formBackingObject + * @return the initial page number + * @see #getInitialPage(HttpServletRequest) + * @see #formBackingObject + */ + protected int getInitialPage(HttpServletRequest request, Object command) { + return getInitialPage(request); + } + + /** + * Return the initial page of the wizard, that is, the page shown at wizard startup. + *

The default implementation returns 0 for first page. + * @param request current HTTP request + * @return the initial page number + */ + protected int getInitialPage(HttpServletRequest request) { + return 0; + } + + /** + * Return the name of the HttpSession attribute that holds the page object + * for this wizard form controller. + *

The default implementation delegates to the {@link #getPageSessionAttributeName()} + * variant without arguments. + * @param request current HTTP request + * @return the name of the form session attribute, or null if not in session form mode + * @see #getPageSessionAttributeName + * @see #getFormSessionAttributeName(javax.servlet.http.HttpServletRequest) + * @see javax.servlet.http.HttpSession#getAttribute + */ + protected String getPageSessionAttributeName(HttpServletRequest request) { + return getPageSessionAttributeName(); + } + + /** + * Return the name of the HttpSession attribute that holds the page object + * for this wizard form controller. + *

Default is an internal name, of no relevance to applications, as the form + * session attribute is not usually accessed directly. Can be overridden to use + * an application-specific attribute name, which allows other code to access + * the session attribute directly. + * @return the name of the page session attribute + * @see #getFormSessionAttributeName + * @see javax.servlet.http.HttpSession#getAttribute + */ + protected String getPageSessionAttributeName() { + return getClass().getName() + ".PAGE." + getCommandName(); + } + + /** + * Handle an invalid submit request, e.g. when in session form mode but no form object + * was found in the session (like in case of an invalid resubmit by the browser). + *

The default implementation for wizard form controllers simply shows the initial page + * of a new wizard form. If you want to show some "invalid submit" message, you need + * to override this method. + * @param request current HTTP request + * @param response current HTTP response + * @return a prepared view, or null if handled directly + * @throws Exception in case of errors + * @see #showNewForm + * @see #setBindOnNewForm + */ + protected ModelAndView handleInvalidSubmit(HttpServletRequest request, HttpServletResponse response) + throws Exception { + + return showNewForm(request, response); + } + + + /** + * Apply wizard workflow: finish, cancel, page change. + */ + protected final ModelAndView processFormSubmission( + HttpServletRequest request, HttpServletResponse response, Object command, BindException errors) + throws Exception { + + int currentPage = getCurrentPage(request); + // Remove page session attribute, provide copy as request attribute. + String pageAttrName = getPageSessionAttributeName(request); + if (isSessionForm()) { + if (logger.isDebugEnabled()) { + logger.debug("Removing page session attribute [" + pageAttrName + "]"); + } + request.getSession().removeAttribute(pageAttrName); + } + request.setAttribute(pageAttrName, new Integer(currentPage)); + + // cancel? + if (isCancelRequest(request)) { + if (logger.isDebugEnabled()) { + logger.debug("Cancelling wizard for form bean '" + getCommandName() + "'"); + } + return processCancel(request, response, command, errors); + } + + // finish? + if (isFinishRequest(request)) { + if (logger.isDebugEnabled()) { + logger.debug("Finishing wizard for form bean '" + getCommandName() + "'"); + } + return validatePagesAndFinish(request, response, command, errors, currentPage); + } + + // Normal submit: validate current page and show specified target page. + if (!suppressValidation(request, command, errors)) { + if (logger.isDebugEnabled()) { + logger.debug("Validating wizard page " + currentPage + " for form bean '" + getCommandName() + "'"); + } + validatePage(command, errors, currentPage, false); + } + + // Give subclasses a change to perform custom post-procession + // of the current page and its command object. + postProcessPage(request, command, errors, currentPage); + + int targetPage = getTargetPage(request, command, errors, currentPage); + if (logger.isDebugEnabled()) { + logger.debug("Target page " + targetPage + " requested"); + } + if (targetPage != currentPage) { + if (!errors.hasErrors() || (this.allowDirtyBack && targetPage < currentPage) || + (this.allowDirtyForward && targetPage > currentPage)) { + // Allowed to go to target page. + return showPage(request, errors, targetPage); + } + } + + // Show current page again. + return showPage(request, errors, currentPage); + } + + /** + * Return the current page number. Used by {@link #processFormSubmission}. + *

The default implementation checks the page session attribute. + * Subclasses can override this for customized page determination. + * @param request current HTTP request + * @return the current page number + * @see #getPageSessionAttributeName() + */ + protected int getCurrentPage(HttpServletRequest request) { + // Check for overriding attribute in request. + String pageAttrName = getPageSessionAttributeName(request); + Integer pageAttr = (Integer) request.getAttribute(pageAttrName); + if (pageAttr != null) { + return pageAttr.intValue(); + } + // Check for explicit request parameter. + String pageParam = request.getParameter(PARAM_PAGE); + if (pageParam != null) { + return Integer.parseInt(pageParam); + } + // Check for original attribute in session. + if (isSessionForm()) { + pageAttr = (Integer) request.getSession().getAttribute(pageAttrName); + if (pageAttr != null) { + return pageAttr.intValue(); + } + } + throw new IllegalStateException( + "Page attribute [" + pageAttrName + "] neither found in session nor in request"); + } + + /** + * Determine whether the incoming request is a request to finish the + * processing of the current form. + *

By default, this method returns true if a parameter + * matching the "_finish" key is present in the request, otherwise it + * returns false. Subclasses may override this method + * to provide custom logic to detect a finish request. + *

The parameter is recognized both when sent as a plain parameter + * ("_finish") or when triggered by an image button ("_finish.x"). + * @param request current HTTP request + * @return whether the request indicates to finish form processing + * @see #PARAM_FINISH + */ + protected boolean isFinishRequest(HttpServletRequest request) { + return WebUtils.hasSubmitParameter(request, PARAM_FINISH); + } + + /** + * Determine whether the incoming request is a request to cancel the + * processing of the current form. + *

By default, this method returns true if a parameter + * matching the "_cancel" key is present in the request, otherwise it + * returns false. Subclasses may override this method + * to provide custom logic to detect a cancel request. + *

The parameter is recognized both when sent as a plain parameter + * ("_cancel") or when triggered by an image button ("_cancel.x"). + * @return whether the request indicates to cancel form processing + * @param request current HTTP request + * @see #PARAM_CANCEL + */ + protected boolean isCancelRequest(HttpServletRequest request) { + return WebUtils.hasSubmitParameter(request, PARAM_CANCEL); + } + + /** + * Return the target page specified in the request. + *

The default implementation delegates to {@link #getTargetPage(HttpServletRequest, int)}. + * Subclasses can override this for customized target page determination. + * @param request current HTTP request + * @param command form object with request parameters bound onto it + * @param errors validation errors holder + * @param currentPage the current page, to be returned as fallback + * if no target page specified + * @return the page specified in the request, or current page if not found + * @see #getTargetPage(HttpServletRequest, int) + */ + protected int getTargetPage(HttpServletRequest request, Object command, Errors errors, int currentPage) { + return getTargetPage(request, currentPage); + } + + /** + * Return the target page specified in the request. + *

The default implementation examines "_target" parameter (e.g. "_target1"). + * Subclasses can override this for customized target page determination. + * @param request current HTTP request + * @param currentPage the current page, to be returned as fallback + * if no target page specified + * @return the page specified in the request, or current page if not found + * @see #PARAM_TARGET + */ + protected int getTargetPage(HttpServletRequest request, int currentPage) { + return WebUtils.getTargetPage(request, PARAM_TARGET, currentPage); + } + + /** + * Validate all pages and process finish. + * If there are page validation errors, show the corresponding view page. + */ + private ModelAndView validatePagesAndFinish( + HttpServletRequest request, HttpServletResponse response, Object command, BindException errors, + int currentPage) throws Exception { + + // In case of binding errors -> show current page. + if (errors.hasErrors()) { + return showPage(request, errors, currentPage); + } + + if (!suppressValidation(request, command, errors)) { + // In case of remaining errors on a page -> show the page. + for (int page = 0; page < getPageCount(request, command); page++) { + validatePage(command, errors, page, true); + if (errors.hasErrors()) { + return showPage(request, errors, page); + } + } + } + + // No remaining errors -> proceed with finish. + return processFinish(request, response, command, errors); + } + + + /** + * Template method for custom validation logic for individual pages. + * The default implementation calls {@link #validatePage(Object, Errors, int)}. + *

Implementations will typically call fine-granular validateXXX + * methods of this instance's Validator, combining them to validation of the + * corresponding pages. The Validator's default validate method + * will not be called by a wizard form controller! + * @param command form object with the current wizard state + * @param errors validation errors holder + * @param page number of page to validate + * @param finish whether this method is called during final revalidation on finish + * (else, it is called for validating the current page) + * @see #validatePage(Object, Errors, int) + * @see org.springframework.validation.Validator#validate + */ + protected void validatePage(Object command, Errors errors, int page, boolean finish) { + validatePage(command, errors, page); + } + + /** + * Template method for custom validation logic for individual pages. + * The default implementation is empty. + *

Implementations will typically call fine-granular validateXXX methods of this + * instance's validator, combining them to validation of the corresponding pages. + * The validator's default validate method will not be called by a + * wizard form controller! + * @param command form object with the current wizard state + * @param errors validation errors holder + * @param page number of page to validate + * @see org.springframework.validation.Validator#validate + */ + protected void validatePage(Object command, Errors errors, int page) { + } + + /** + * Post-process the given page after binding and validation, potentially + * updating its command object. The passed-in request might contain special + * parameters sent by the page. + *

Only invoked when displaying another page or the same page again, + * not when finishing or cancelling. + * @param request current HTTP request + * @param command form object with request parameters bound onto it + * @param errors validation errors holder + * @param page number of page to post-process + * @throws Exception in case of invalid state or arguments + */ + protected void postProcessPage(HttpServletRequest request, Object command, Errors errors, int page) + throws Exception { + } + + /** + * Template method for processing the final action of this wizard. + *

Call errors.getModel() to populate the ModelAndView model + * with the command and the Errors instance, under the specified command name, + * as expected by the "spring:bind" tag. + *

You can call the {@link #showPage} method to return back to the wizard, + * in case of last-minute validation errors having been found that you would + * like to present to the user within the original wizard form. + * @param request current HTTP request + * @param response current HTTP response + * @param command form object with the current wizard state + * @param errors validation errors holder + * @return the finish view + * @throws Exception in case of invalid state or arguments + * @see org.springframework.validation.Errors + * @see org.springframework.validation.BindException#getModel + * @see #showPage(javax.servlet.http.HttpServletRequest, org.springframework.validation.BindException, int) + */ + protected abstract ModelAndView processFinish( + HttpServletRequest request, HttpServletResponse response, Object command, BindException errors) + throws Exception; + + /** + * Template method for processing the cancel action of this wizard. + *

The default implementation throws a ServletException, saying that a cancel + * operation is not supported by this controller. Thus, you do not need to + * implement this template method if you do not support a cancel operation. + *

Call errors.getModel() to populate the ModelAndView model + * with the command and the Errors instance, under the specified command name, + * as expected by the "spring:bind" tag. + * @param request current HTTP request + * @param response current HTTP response + * @param command form object with the current wizard state + * @param errors Errors instance containing errors + * @return the cancellation view + * @throws Exception in case of invalid state or arguments + * @see org.springframework.validation.Errors + * @see org.springframework.validation.BindException#getModel + */ + protected ModelAndView processCancel( + HttpServletRequest request, HttpServletResponse response, Object command, BindException errors) + throws Exception { + + throw new ServletException( + "Wizard form controller class [" + getClass().getName() + "] does not support a cancel operation"); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/BaseCommandController.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/BaseCommandController.java new file mode 100644 index 00000000000..b7e7f44f4ab --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/BaseCommandController.java @@ -0,0 +1,591 @@ +/* + * Copyright 2002-2007 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.web.servlet.mvc; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.PropertyEditorRegistrar; +import org.springframework.validation.BindException; +import org.springframework.validation.BindingErrorProcessor; +import org.springframework.validation.MessageCodesResolver; +import org.springframework.validation.ValidationUtils; +import org.springframework.validation.Validator; +import org.springframework.web.bind.ServletRequestDataBinder; +import org.springframework.web.bind.support.WebBindingInitializer; +import org.springframework.web.context.request.ServletWebRequest; + +/** + *

Controller implementation which creates an object (the command object) on + * receipt of a request and attempts to populate this object with request parameters.

+ * + *

This controller is the base for all controllers wishing to populate + * JavaBeans based on request parameters, validate the content of such + * JavaBeans using {@link org.springframework.validation.Validator Validators} + * and use custom editors (in the form of + * {@link java.beans.PropertyEditor PropertyEditors}) to transform + * objects into strings and vice versa, for example. Three notions are mentioned here:

+ * + *

Command class:
+ * An instance of the command class will be created for each request and populated + * with request parameters. A command class can basically be any Java class; the only + * requirement is a no-arg constructor. The command class should preferably be a + * JavaBean in order to be able to populate bean properties with request parameters.

+ * + *

Populating using request parameters and PropertyEditors:
+ * Upon receiving a request, any BaseCommandController will attempt to fill the + * command object using the request parameters. This is done using the typical + * and well-known JavaBeans property notation. When a request parameter named + * 'firstName' exists, the framework will attempt to call + * setFirstName([value]) passing the value of the parameter. Nested properties + * are of course supported. For instance a parameter named 'address.city' + * will result in a getAddress().setCity([value]) call on the + * command class.

+ * + *

It's important to realise that you are not limited to String arguments in + * your JavaBeans. Using the PropertyEditor-notion as supplied by the + * java.beans package, you will be able to transform Strings to Objects and + * the other way around. For instance setLocale(Locale loc) is + * perfectly possible for a request parameter named locale having + * a value of en, as long as you register the appropriate + * PropertyEditor in the Controller (see {@link #initBinder initBinder()} + * for more information on that matter.

+ * + *

Validators: + * After the controller has successfully populated the command object with + * parameters from the request, it will use any configured validators to + * validate the object. Validation results will be put in a + * {@link org.springframework.validation.Errors Errors} object which can be + * used in a View to render any input problems.

+ * + *

Workflow + * (and that defined by superclass):
+ * Since this class is an abstract base class for more specific implementation, + * it does not override the handleRequestInternal() method and also has no + * actual workflow. Implementing classes like + * {@link AbstractFormController AbstractFormController}, + * {@link AbstractCommandController AbstractcommandController}, + * {@link SimpleFormController SimpleFormController} and + * {@link AbstractWizardFormController AbstractWizardFormController} + * provide actual functionality and workflow. + * More information on workflow performed by superclasses can be found + * here.

+ * + *

Exposed configuration properties + * (and those defined by superclass):
+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
name + * defaultdescription
commandNamecommandthe name to use when binding the instantiated command class + * to the request
commandClassnullthe class to use upon receiving a request and which to fill + * using the request parameters. What object is used and whether + * or not it should be created is defined by extending classes + * and their configuration properties and methods.
validatorsnullArray of Validator beans. The validator will be called at appropriate + * places in the workflow of subclasses (have a look at those for more info) + * to validate the command object.
validatornullShort-form property for setting only one Validator bean (usually passed in + * using a <ref bean="beanId"/> property.
validateOnBindingtrueIndicates whether or not to validate the command object after the + * object has been populated with request parameters.
+ *

+ * + * @author Rod Johnson + * @author Juergen Hoeller + */ +public abstract class BaseCommandController extends AbstractController { + + /** Default command name used for binding command objects: "command" */ + public static final String DEFAULT_COMMAND_NAME = "command"; + + + private String commandName = DEFAULT_COMMAND_NAME; + + private Class commandClass; + + private Validator[] validators; + + private boolean validateOnBinding = true; + + private MessageCodesResolver messageCodesResolver; + + private BindingErrorProcessor bindingErrorProcessor; + + private PropertyEditorRegistrar[] propertyEditorRegistrars; + + private WebBindingInitializer webBindingInitializer; + + + /** + * Set the name of the command in the model. + * The command object will be included in the model under this name. + */ + public final void setCommandName(String commandName) { + this.commandName = commandName; + } + + /** + * Return the name of the command in the model. + */ + public final String getCommandName() { + return this.commandName; + } + + /** + * Set the command class for this controller. + * An instance of this class gets populated and validated on each request. + */ + public final void setCommandClass(Class commandClass) { + this.commandClass = commandClass; + } + + /** + * Return the command class for this controller. + */ + public final Class getCommandClass() { + return this.commandClass; + } + + /** + * Set the primary Validator for this controller. The Validator + * must support the specified command class. If there are one + * or more existing validators set already when this method is + * called, only the specified validator will be kept. Use + * {@link #setValidators(Validator[])} to set multiple validators. + */ + public final void setValidator(Validator validator) { + this.validators = new Validator[] {validator}; + } + + /** + * Return the primary Validator for this controller. + */ + public final Validator getValidator() { + return (this.validators != null && this.validators.length > 0 ? this.validators[0] : null); + } + + /** + * Set the Validators for this controller. + * The Validator must support the specified command class. + */ + public final void setValidators(Validator[] validators) { + this.validators = validators; + } + + /** + * Return the Validators for this controller. + */ + public final Validator[] getValidators() { + return this.validators; + } + + /** + * Set if the Validator should get applied when binding. + */ + public final void setValidateOnBinding(boolean validateOnBinding) { + this.validateOnBinding = validateOnBinding; + } + + /** + * Return if the Validator should get applied when binding. + */ + public final boolean isValidateOnBinding() { + return this.validateOnBinding; + } + + /** + * Set the strategy to use for resolving errors into message codes. + * Applies the given strategy to all data binders used by this controller. + *

Default is null, i.e. using the default strategy of + * the data binder. + * @see #createBinder + * @see org.springframework.validation.DataBinder#setMessageCodesResolver + */ + public final void setMessageCodesResolver(MessageCodesResolver messageCodesResolver) { + this.messageCodesResolver = messageCodesResolver; + } + + /** + * Return the strategy to use for resolving errors into message codes (if any). + */ + public final MessageCodesResolver getMessageCodesResolver() { + return this.messageCodesResolver; + } + + /** + * Set the strategy to use for processing binding errors, that is, + * required field errors and PropertyAccessExceptions. + *

Default is null, that is, using the default strategy + * of the data binder. + * @see #createBinder + * @see org.springframework.validation.DataBinder#setBindingErrorProcessor + */ + public final void setBindingErrorProcessor(BindingErrorProcessor bindingErrorProcessor) { + this.bindingErrorProcessor = bindingErrorProcessor; + } + + /** + * Return the strategy to use for processing binding errors (if any). + */ + public final BindingErrorProcessor getBindingErrorProcessor() { + return this.bindingErrorProcessor; + } + + /** + * Specify a single PropertyEditorRegistrar to be applied + * to every DataBinder that this controller uses. + *

Allows for factoring out the registration of PropertyEditors + * to separate objects, as an alternative to {@link #initBinder}. + * @see #initBinder + */ + public final void setPropertyEditorRegistrar(PropertyEditorRegistrar propertyEditorRegistrar) { + this.propertyEditorRegistrars = new PropertyEditorRegistrar[] {propertyEditorRegistrar}; + } + + /** + * Specify multiple PropertyEditorRegistrars to be applied + * to every DataBinder that this controller uses. + *

Allows for factoring out the registration of PropertyEditors + * to separate objects, as an alternative to {@link #initBinder}. + * @see #initBinder + */ + public final void setPropertyEditorRegistrars(PropertyEditorRegistrar[] propertyEditorRegistrars) { + this.propertyEditorRegistrars = propertyEditorRegistrars; + } + + /** + * Return the PropertyEditorRegistrars (if any) to be applied + * to every DataBinder that this controller uses. + */ + public final PropertyEditorRegistrar[] getPropertyEditorRegistrars() { + return this.propertyEditorRegistrars; + } + + /** + * Specify a WebBindingInitializer which will apply pre-configured + * configuration to every DataBinder that this controller uses. + *

Allows for factoring out the entire binder configuration + * to separate objects, as an alternative to {@link #initBinder}. + */ + public final void setWebBindingInitializer(WebBindingInitializer webBindingInitializer) { + this.webBindingInitializer = webBindingInitializer; + } + + /** + * Return the WebBindingInitializer (if any) which will apply pre-configured + * configuration to every DataBinder that this controller uses. + */ + public final WebBindingInitializer getWebBindingInitializer() { + return this.webBindingInitializer; + } + + + protected void initApplicationContext() { + if (this.validators != null) { + for (int i = 0; i < this.validators.length; i++) { + if (this.commandClass != null && !this.validators[i].supports(this.commandClass)) + throw new IllegalArgumentException("Validator [" + this.validators[i] + + "] does not support command class [" + + this.commandClass.getName() + "]"); + } + } + } + + + /** + * Retrieve a command object for the given request. + *

The default implementation calls {@link #createCommand}. + * Subclasses can override this. + * @param request current HTTP request + * @return object command to bind onto + * @throws Exception if the command object could not be obtained + * @see #createCommand + */ + protected Object getCommand(HttpServletRequest request) throws Exception { + return createCommand(); + } + + /** + * Create a new command instance for the command class of this controller. + *

This implementation uses BeanUtils.instantiateClass, + * so the command needs to have a no-arg constructor (supposed to be + * public, but not required to). + * @return the new command instance + * @throws Exception if the command object could not be instantiated + * @see org.springframework.beans.BeanUtils#instantiateClass(Class) + */ + protected final Object createCommand() throws Exception { + if (this.commandClass == null) { + throw new IllegalStateException("Cannot create command without commandClass being set - " + + "either set commandClass or (in a form controller) override formBackingObject"); + } + if (logger.isDebugEnabled()) { + logger.debug("Creating new command of class [" + this.commandClass.getName() + "]"); + } + return BeanUtils.instantiateClass(this.commandClass); + } + + /** + * Check if the given command object is a valid for this controller, + * i.e. its command class. + * @param command the command object to check + * @return if the command object is valid for this controller + */ + protected final boolean checkCommand(Object command) { + return (this.commandClass == null || this.commandClass.isInstance(command)); + } + + + /** + * Bind the parameters of the given request to the given command object. + * @param request current HTTP request + * @param command the command to bind onto + * @return the ServletRequestDataBinder instance for additional custom validation + * @throws Exception in case of invalid state or arguments + */ + protected final ServletRequestDataBinder bindAndValidate(HttpServletRequest request, Object command) + throws Exception { + + ServletRequestDataBinder binder = createBinder(request, command); + BindException errors = new BindException(binder.getBindingResult()); + if (!suppressBinding(request)) { + binder.bind(request); + onBind(request, command, errors); + if (this.validators != null && isValidateOnBinding() && !suppressValidation(request, command, errors)) { + for (int i = 0; i < this.validators.length; i++) { + ValidationUtils.invokeValidator(this.validators[i], command, errors); + } + } + onBindAndValidate(request, command, errors); + } + return binder; + } + + /** + * Return whether to suppress binding for the given request. + *

The default implementation always returns "false". Can be overridden + * in subclasses to suppress validation, for example, if a special + * request parameter is set. + * @param request current HTTP request + * @return whether to suppress binding for the given request + * @see #suppressValidation + */ + protected boolean suppressBinding(HttpServletRequest request) { + return false; + } + + /** + * Create a new binder instance for the given command and request. + *

Called by {@link #bindAndValidate}. Can be overridden to plug in + * custom ServletRequestDataBinder instances. + *

The default implementation creates a standard ServletRequestDataBinder + * and invokes {@link #prepareBinder} and {@link #initBinder}. + *

Note that neither {@link #prepareBinder} nor {@link #initBinder} will + * be invoked automatically if you override this method! Call those methods + * at appropriate points of your overridden method. + * @param request current HTTP request + * @param command the command to bind onto + * @return the new binder instance + * @throws Exception in case of invalid state or arguments + * @see #bindAndValidate + * @see #prepareBinder + * @see #initBinder + */ + protected ServletRequestDataBinder createBinder(HttpServletRequest request, Object command) + throws Exception { + + ServletRequestDataBinder binder = new ServletRequestDataBinder(command, getCommandName()); + prepareBinder(binder); + initBinder(request, binder); + return binder; + } + + /** + * Prepare the given binder, applying the specified MessageCodesResolver, + * BindingErrorProcessor and PropertyEditorRegistrars (if any). + * Called by {@link #createBinder}. + * @param binder the new binder instance + * @see #createBinder + * @see #setMessageCodesResolver + * @see #setBindingErrorProcessor + */ + protected final void prepareBinder(ServletRequestDataBinder binder) { + if (useDirectFieldAccess()) { + binder.initDirectFieldAccess(); + } + if (this.messageCodesResolver != null) { + binder.setMessageCodesResolver(this.messageCodesResolver); + } + if (this.bindingErrorProcessor != null) { + binder.setBindingErrorProcessor(this.bindingErrorProcessor); + } + if (this.propertyEditorRegistrars != null) { + for (int i = 0; i < this.propertyEditorRegistrars.length; i++) { + this.propertyEditorRegistrars[i].registerCustomEditors(binder); + } + } + } + + /** + * Determine whether to use direct field access instead of bean property access. + * Applied by {@link #prepareBinder}. + *

Default is "false". Can be overridden in subclasses. + * @return whether to use direct field access (true) + * or bean property access (false) + * @see #prepareBinder + * @see org.springframework.validation.DataBinder#initDirectFieldAccess() + */ + protected boolean useDirectFieldAccess() { + return false; + } + + /** + * Initialize the given binder instance, for example with custom editors. + * Called by {@link #createBinder}. + *

This method allows you to register custom editors for certain fields of your + * command class. For instance, you will be able to transform Date objects into a + * String pattern and back, in order to allow your JavaBeans to have Date properties + * and still be able to set and display them in an HTML interface. + *

The default implementation is empty. + * @param request current HTTP request + * @param binder the new binder instance + * @throws Exception in case of invalid state or arguments + * @see #createBinder + * @see org.springframework.validation.DataBinder#registerCustomEditor + * @see org.springframework.beans.propertyeditors.CustomDateEditor + */ + protected void initBinder(HttpServletRequest request, ServletRequestDataBinder binder) throws Exception { + if (this.webBindingInitializer != null) { + this.webBindingInitializer.initBinder(binder, new ServletWebRequest(request)); + } + } + + /** + * Callback for custom post-processing in terms of binding. + * Called on each submit, after standard binding but before validation. + *

The default implementation delegates to {@link #onBind(HttpServletRequest, Object)}. + * @param request current HTTP request + * @param command the command object to perform further binding on + * @param errors validation errors holder, allowing for additional + * custom registration of binding errors + * @throws Exception in case of invalid state or arguments + * @see #bindAndValidate + * @see #onBind(HttpServletRequest, Object) + */ + protected void onBind(HttpServletRequest request, Object command, BindException errors) throws Exception { + onBind(request, command); + } + + /** + * Callback for custom post-processing in terms of binding. + *

Called by the default implementation of the + * {@link #onBind(HttpServletRequest, Object, BindException)} variant + * with all parameters, after standard binding but before validation. + *

The default implementation is empty. + * @param request current HTTP request + * @param command the command object to perform further binding on + * @throws Exception in case of invalid state or arguments + * @see #onBind(HttpServletRequest, Object, BindException) + */ + protected void onBind(HttpServletRequest request, Object command) throws Exception { + } + + /** + * Return whether to suppress validation for the given request. + *

The default implementation delegates to {@link #suppressValidation(HttpServletRequest, Object)}. + * @param request current HTTP request + * @param command the command object to validate + * @param errors validation errors holder, allowing for additional + * custom registration of binding errors + * @return whether to suppress validation for the given request + */ + protected boolean suppressValidation(HttpServletRequest request, Object command, BindException errors) { + return suppressValidation(request, command); + } + + /** + * Return whether to suppress validation for the given request. + *

Called by the default implementation of the + * {@link #suppressValidation(HttpServletRequest, Object, BindException)} variant + * with all parameters. + *

The default implementation delegates to {@link #suppressValidation(HttpServletRequest)}. + * @param request current HTTP request + * @param command the command object to validate + * @return whether to suppress validation for the given request + */ + protected boolean suppressValidation(HttpServletRequest request, Object command) { + return suppressValidation(request); + } + + /** + * Return whether to suppress validation for the given request. + *

Called by the default implementation of the + * {@link #suppressValidation(HttpServletRequest, Object)} variant + * with all parameters. + *

The default implementation is empty. + * @param request current HTTP request + * @return whether to suppress validation for the given request + * @deprecated as of Spring 2.0.4, in favor of the + * {@link #suppressValidation(HttpServletRequest, Object)} variant + */ + protected boolean suppressValidation(HttpServletRequest request) { + return false; + } + + /** + * Callback for custom post-processing in terms of binding and validation. + * Called on each submit, after standard binding and validation, + * but before error evaluation. + *

The default implementation is empty. + * @param request current HTTP request + * @param command the command object, still allowing for further binding + * @param errors validation errors holder, allowing for additional + * custom validation + * @throws Exception in case of invalid state or arguments + * @see #bindAndValidate + * @see org.springframework.validation.Errors + */ + protected void onBindAndValidate(HttpServletRequest request, Object command, BindException errors) + throws Exception { + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/CancellableFormController.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/CancellableFormController.java new file mode 100644 index 00000000000..b0bba252d8d --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/CancellableFormController.java @@ -0,0 +1,205 @@ +/* + * Copyright 2002-2007 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.web.servlet.mvc; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.validation.BindException; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.util.WebUtils; + +/** + *

Extension of SimpleFormController that supports "cancellation" + * of form processing. By default, this controller looks for a given parameter in the + * request, identified by the cancelParamKey. If this parameter is present, + * then the controller will return the configured cancelView, otherwise + * processing is passed back to the superclass.

+ * + *

Workflow + * (in addition to the superclass):
+ *

    + *
  1. Call to {@link #processFormSubmission processFormSubmission} which calls + * {@link #isCancelRequest} to see if the incoming request is to cancel the + * current form entry. By default, {@link #isCancelRequest} returns true + * if the configured cancelParamKey exists in the request. + * This behavior can be overridden in subclasses.
  2. + *
  3. If {@link #isCancelRequest} returns false, then the controller + * will delegate all processing back to {@link SimpleFormController SimpleFormController}, + * otherwise it will call the {@link #onCancel} version with all parameters. + * By default, that method will delegate to the {@link #onCancel} version with just + * the command object, which will in turn simply return the configured + * cancelView. This behavior can be overridden in subclasses.
  4. + *
+ *

+ * + *

Thanks to Erwin Bolwidt for submitting the original prototype + * of such a cancellable form controller!

+ * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2.3 + * @see #setCancelParamKey + * @see #setCancelView + * @see #isCancelRequest(javax.servlet.http.HttpServletRequest) + * @see #onCancel(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, Object) + */ +public class CancellableFormController extends SimpleFormController { + + /** + * Default parameter triggering the cancel action. + * Can be called even with validation errors on the form. + */ + private static final String PARAM_CANCEL = "_cancel"; + + + private String cancelParamKey = PARAM_CANCEL; + + private String cancelView; + + + /** + * Set the key of the request parameter used to identify a cancel request. + * Default is "_cancel". + *

The parameter is recognized both when sent as a plain parameter + * ("_cancel") or when triggered by an image button ("_cancel.x"). + */ + public final void setCancelParamKey(String cancelParamKey) { + this.cancelParamKey = cancelParamKey; + } + + /** + * Return the key of the request parameter used to identify a cancel request. + */ + public final String getCancelParamKey() { + return this.cancelParamKey; + } + + /** + * Sets the name of the cancel view. + */ + public final void setCancelView(String cancelView) { + this.cancelView = cancelView; + } + + /** + * Gets the name of the cancel view. + */ + public final String getCancelView() { + return this.cancelView; + } + + + /** + * Consider an explicit cancel request as a form submission too. + * @see #isCancelRequest(javax.servlet.http.HttpServletRequest) + */ + protected boolean isFormSubmission(HttpServletRequest request) { + return super.isFormSubmission(request) || isCancelRequest(request); + } + + /** + * Suppress validation for an explicit cancel request too. + * @see #isCancelRequest(javax.servlet.http.HttpServletRequest) + */ + protected boolean suppressValidation(HttpServletRequest request, Object command) { + return super.suppressValidation(request, command) || isCancelRequest(request); + } + + /** + * This implementation first checks to see if the incoming is a cancel request, + * through a call to {@link #isCancelRequest}. If so, control is passed to + * {@link #onCancel}; otherwise, control is passed up to + * {@link SimpleFormController#processFormSubmission}. + * @see #isCancelRequest + * @see #onCancel(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, Object) + * @see SimpleFormController#processFormSubmission + */ + protected ModelAndView processFormSubmission( + HttpServletRequest request, HttpServletResponse response, Object command, BindException errors) + throws Exception { + + if (isCancelRequest(request)) { + return onCancel(request, response, command); + } + else { + return super.processFormSubmission(request, response, command, errors); + } + } + + /** + * Determine whether the incoming request is a request to cancel the + * processing of the current form. + *

By default, this method returns true if a parameter + * matching the configured cancelParamKey is present in + * the request, otherwise it returns false. Subclasses may + * override this method to provide custom logic to detect a cancel request. + *

The parameter is recognized both when sent as a plain parameter + * ("_cancel") or when triggered by an image button ("_cancel.x"). + * @param request current HTTP request + * @see #setCancelParamKey + * @see #PARAM_CANCEL + */ + protected boolean isCancelRequest(HttpServletRequest request) { + return WebUtils.hasSubmitParameter(request, getCancelParamKey()); + } + + /** + * Callback method for handling a cancel request. Called if {@link #isCancelRequest} + * returns true. + *

Default implementation delegates to onCancel(Object) to return + * the configured cancelView. Subclasses may override either of the two + * methods to build a custom {@link ModelAndView ModelAndView} that may contain model + * parameters used in the cancel view. + *

If you simply want to move the user to a new view and you don't want to add + * additional model parameters, use {@link #setCancelView(String)} rather than + * overriding an onCancel method. + * @param request current servlet request + * @param response current servlet response + * @param command form object with request parameters bound onto it + * @return the prepared model and view, or null + * @throws Exception in case of errors + * @see #isCancelRequest(javax.servlet.http.HttpServletRequest) + * @see #onCancel(Object) + * @see #setCancelView + */ + protected ModelAndView onCancel(HttpServletRequest request, HttpServletResponse response, Object command) + throws Exception { + + return onCancel(command); + } + + /** + * Simple onCancel version. Called by the default implementation + * of the onCancel version with all parameters. + *

Default implementation returns eturns the configured cancelView. + * Subclasses may override this method to build a custom {@link ModelAndView ModelAndView} + * that may contain model parameters used in the cancel view. + *

If you simply want to move the user to a new view and you don't want to add + * additional model parameters, use {@link #setCancelView(String)} rather than + * overriding an onCancel method. + * @param command form object with request parameters bound onto it + * @return the prepared model and view, or null + * @throws Exception in case of errors + * @see #onCancel(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, Object) + * @see #setCancelView + */ + protected ModelAndView onCancel(Object command) throws Exception { + return new ModelAndView(getCancelView()); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/Controller.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/Controller.java new file mode 100644 index 00000000000..6a4eca977f1 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/Controller.java @@ -0,0 +1,133 @@ +/* + * Copyright 2002-2006 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.web.servlet.mvc; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.web.servlet.ModelAndView; + +/** + * Base Controller interface, representing a component that receives + * HttpServletRequest and HttpServletResponse + * instances just like a HttpServlet but is able to + * participate in an MVC workflow. Controllers are comparable to the + * notion of a Struts Action. + * + *

Any implementation of the Controller interface should be a + * reusable, thread-safe class, capable of handling multiple + * HTTP requests throughout the lifecycle of an application. To be able to + * configure a Controller easily, Controller implementations are encouraged + * to be (and usually are) JavaBeans. + *

+ * + *

Workflow

+ * + *

+ * After a DispatcherServlet has received a request and has + * done its work to resolve locales, themes and suchlike, it then tries + * to resolve a Controller, using a + * {@link org.springframework.web.servlet.HandlerMapping HandlerMapping}. + * When a Controller has been found to handle the request, the + * {@link #handleRequest(HttpServletRequest, HttpServletResponse) handleRequest} + * method of the located Controller will be invoked; the located Controller + * is then responsible for handling the actual request and - if applicable - + * returning an appropriate + * {@link org.springframework.web.servlet.ModelAndView ModelAndView}. + * So actually, this method is the main entrypoint for the + * {@link org.springframework.web.servlet.DispatcherServlet DispatcherServlet} + * which delegates requests to controllers. This method - and also this interface - + * should preferrably not be implemented by custom controllers directly, since + * abstract controller also provided by this package already provide a lot of + * functionality for typical use cases in web applications. A few examples of + * those controllers: + * {@link AbstractController AbstractController}, + * {@link AbstractCommandController AbstractCommandController}, + * {@link SimpleFormController SimpleFormController}.

+ * + *

So basically any direct implementation of the Controller interface + * just handles HttpServletRequests and should return a ModelAndView, to be further + * interpreted by the DispatcherServlet. Any additional functionality such as + * optional validation, form handling, etc should be obtained through extending + * one of the abstract controller classes mentioned above.

+ * + *

Notes on design and testing

+ * + *

The Controller interface is explicitly designed to operate on HttpServletRequest + * and HttpServletResponse objects, just like an HttpServlet. It does not aim to + * decouple itself from the Servlet API, in contrast to, for example, WebWork, JSF or Tapestry. + * Instead, the full power of the Servlet API is available, allowing Controllers to be + * general-purpose: a Controller is able to not only handle web user interface + * requests but also to process remoting protocols or to generate reports on demand.

+ * + *

Controllers can easily be tested by passing in mock objects for the + * HttpServletRequest and HttpServletResponse objects as parameters to the + * {@link #handleRequest(HttpServletRequest, HttpServletResponse) handleRequest} + * method. As a convenience, Spring ships with a set of Servlet API mocks + * that are suitable for testing any kind of web components, but are particularly + * suitable for testing Spring web controllers. In contrast to a Struts Action, + * there is no need to mock the ActionServlet or any other infrastructure; + * HttpServletRequest and HttpServletResponse are sufficient.

+ * + *

If Controllers need to be aware of specific environment references, they can + * choose to implement specific awareness interfaces, just like any other bean in a + * Spring (web) application context can do, for example:

+ *
    + *
  • org.springframework.context.ApplicationContextAware
  • + *
  • org.springframework.context.ResourceLoaderAware
  • + *
  • org.springframework.web.context.ServletContextAware
  • + *
+ * + *

Such environment references can easily be passed in testing environments, + * through the corresponding setters defined in the respective awareness interfaces. + * In general, it is recommended to keep the dependencies as minimal as possible: + * for example, if all you need is resource loading, implement ResourceLoaderAware only. + * Alternatively, derive from the WebApplicationObjectSupport base class, which gives + * you all those references through convenient accessors - but requires an + * ApplicationContext reference on initialization. + * + *

Controllers can optionally implement the LastModified interface. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see LastModified + * @see SimpleControllerHandlerAdapter + * @see AbstractController + * @see AbstractCommandController + * @see SimpleFormController + * @see org.springframework.mock.web.MockHttpServletRequest + * @see org.springframework.mock.web.MockHttpServletResponse + * @see org.springframework.context.ApplicationContextAware + * @see org.springframework.context.ResourceLoaderAware + * @see org.springframework.web.context.ServletContextAware + * @see org.springframework.web.context.support.WebApplicationObjectSupport + */ +public interface Controller { + + /** + * Process the request and return a ModelAndView object which the DispatcherServlet + * will render. A null return value is not an error: It indicates that + * this object completed request processing itself, thus there is no ModelAndView + * to render. + * @param request current HTTP request + * @param response current HTTP response + * @return a ModelAndView to render, or null if handled directly + * @throws Exception in case of errors + */ + ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception; + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/HttpRequestHandlerAdapter.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/HttpRequestHandlerAdapter.java new file mode 100644 index 00000000000..fd8650420ce --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/HttpRequestHandlerAdapter.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2007 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.web.servlet.mvc; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.web.HttpRequestHandler; +import org.springframework.web.servlet.HandlerAdapter; +import org.springframework.web.servlet.ModelAndView; + +/** + * Adapter to use the plain {@link org.springframework.web.HttpRequestHandler} + * interface with the generic {@link org.springframework.web.servlet.DispatcherServlet}. + * Supports handlers that implement the {@link LastModified} interface. + * + *

This is an SPI class, not used directly by application code. + * + * @author Juergen Hoeller + * @since 2.0 + * @see org.springframework.web.servlet.DispatcherServlet + * @see org.springframework.web.HttpRequestHandler + * @see LastModified + * @see SimpleControllerHandlerAdapter + */ +public class HttpRequestHandlerAdapter implements HandlerAdapter { + + public boolean supports(Object handler) { + return (handler instanceof HttpRequestHandler); + } + + public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + + ((HttpRequestHandler) handler).handleRequest(request, response); + return null; + } + + public long getLastModified(HttpServletRequest request, Object handler) { + if (handler instanceof LastModified) { + return ((LastModified) handler).getLastModified(request); + } + return -1L; + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/LastModified.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/LastModified.java new file mode 100644 index 00000000000..7d4ed6dfb83 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/LastModified.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2008 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.web.servlet.mvc; + +import javax.servlet.http.HttpServletRequest; + +/** + * Supports last-modified HTTP requests to facilitate content caching. + * Same contract as for the Servlet API's getLastModified method. + * + *

Delegated to by a {@link org.springframework.web.servlet.HandlerAdapter#getLastModified} + * implementation. By default, any Controller or HttpRequestHandler within Spring's + * default framework can implement this interface to enable last-modified checking. + * + *

Note: Alternative handler implementation approaches have different + * last-modified handling styles. For example, Spring 2.5's annotated controller + * approach (using @RequestMapping) provides last-modified support + * through the {@link org.springframework.web.context.request.WebRequest#checkNotModified} + * method, allowing for last-modified checking within the main handler method. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see javax.servlet.http.HttpServlet#getLastModified + * @see Controller + * @see SimpleControllerHandlerAdapter + * @see org.springframework.web.HttpRequestHandler + * @see HttpRequestHandlerAdapter + */ +public interface LastModified { + + /** + * Same contract as for HttpServlet's getLastModified method. + * Invoked before request processing. + *

The return value will be sent to the HTTP client as Last-Modified header, + * and compared with If-Modified-Since headers that the client sends back. + * The content will only get regenerated if there has been a modification. + * @param request current HTTP request + * @return the time the underlying resource was last modified, or -1 + * meaning that the content must always be regenerated + * @see org.springframework.web.servlet.HandlerAdapter#getLastModified + * @see javax.servlet.http.HttpServlet#getLastModified + */ + long getLastModified(HttpServletRequest request); + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/ParameterizableViewController.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/ParameterizableViewController.java new file mode 100644 index 00000000000..b571b8f57a4 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/ParameterizableViewController.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2007 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.web.servlet.mvc; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.web.servlet.ModelAndView; + +/** + *

Trivial controller that always returns a named view. The view + * can be configured using an exposed configuration property. This + * controller offers an alternative to sending a request straight to a view + * such as a JSP. The advantage here is that the client is not exposed to + * the concrete view technology but rather just to the controller URL; + * the concrete view will be determined by the ViewResolver. + * + *

An alternative to the ParameterizableViewController is a + * {@link org.springframework.web.servlet.mvc.multiaction.MultiActionController MultiActionController}, + * which can define a variety of handler methods that just return a plain + * ModelAndView instance for a given view name. + * + *

Workflow + * (and that defined by superclass):
+ *

    + *
  1. Request is received by the controller
  2. + *
  3. call to {@link #handleRequestInternal handleRequestInternal} which + * just returns the view, named by the configuration property + * viewName. Nothing more, nothing less
  4. + *
+ *

+ * + *

Exposed configuration properties + * (and those defined by superclass):
+ * + * + * + * + * + * + * + * + * + * + * + *
namedefaultdescription
viewNamenullthe name of the view the viewResolver will use to forward to + * (if this property is not set, an exception will be thrown during + * initialization)
+ *

+ * + * @author Rod Johnson + * @author Juergen Hoeller + */ +public class ParameterizableViewController extends AbstractController { + + private String viewName; + + + /** + * Set the name of the view to delegate to. + */ + public void setViewName(String viewName) { + this.viewName = viewName; + } + + /** + * Return the name of the view to delegate to. + */ + public String getViewName() { + return this.viewName; + } + + protected void initApplicationContext() { + if (this.viewName == null) { + throw new IllegalArgumentException("Property 'viewName' is required"); + } + } + + + /** + * Return a ModelAndView object with the specified view name. + * @see #getViewName() + */ + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) + throws Exception { + + return new ModelAndView(getViewName()); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/ServletForwardingController.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/ServletForwardingController.java new file mode 100644 index 00000000000..d1dc8b7e3e1 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/ServletForwardingController.java @@ -0,0 +1,155 @@ +/* + * Copyright 2002-2007 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.web.servlet.mvc; + +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.util.WebUtils; + +/** + * Spring Controller implementation that forwards to a named servlet, + * i.e. the "servlet-name" in web.xml rather than a URL path mapping. + * A target servlet doesn't even need a "servlet-mapping" in web.xml + * in the first place: A "servlet" declaration is sufficient. + * + *

Useful to invoke an existing servlet via Spring's dispatching infrastructure, + * for example to apply Spring HandlerInterceptors to its requests. This will work + * even in a minimal Servlet container that does not support Servlet filters. + * + *

Example: web.xml, mapping all "/myservlet" requests to a Spring dispatcher. + * Also defines a custom "myServlet", but without servlet mapping. + * + *

+ * <servlet>
+ *   <servlet-name>myServlet</servlet-name>
+ *   <servlet-class>mypackage.TestServlet</servlet-class>
+ * </servlet>
+ *
+ * <servlet>
+ *   <servlet-name>myDispatcher</servlet-name>
+ *   <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
+ * </servlet>
+ *
+ * <servlet-mapping>
+ *   <servlet-name>myDispatcher</servlet-name>
+ *   <url-pattern>/myservlet</url-pattern>
+ * </servlet-mapping>
+ * + * Example: myDispatcher-servlet.xml, in turn forwarding "/myservlet" to your + * servlet (identified by servlet name). All such requests will go through the + * configured HandlerInterceptor chain (e.g. an OpenSessionInViewInterceptor). + * From the servlet point of view, everything will work as usual. + * + *
+ * <bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
+ *   <property name="interceptors">
+ *     <list>
+ *       <ref bean="openSessionInViewInterceptor"/>
+ *     </list>
+ *   </property>
+ *   <property name="mappings">
+ *     <props>
+ *       <prop key="/myservlet">myServletForwardingController</prop>
+ *     </props>
+ *   </property>
+ * </bean>
+ *
+ * <bean id="myServletForwardingController" class="org.springframework.web.servlet.mvc.ServletForwardingController">
+ *   <property name="servletName"><value>myServlet</value></property>
+ * </bean>
+ * + * @author Juergen Hoeller + * @since 1.1.1 + * @see ServletWrappingController + * @see org.springframework.orm.hibernate3.support.OpenSessionInViewInterceptor + * @see org.springframework.orm.hibernate3.support.OpenSessionInViewFilter + * @see org.springframework.orm.jdo.support.OpenPersistenceManagerInViewInterceptor + * @see org.springframework.orm.jdo.support.OpenPersistenceManagerInViewFilter + */ +public class ServletForwardingController extends AbstractController implements BeanNameAware { + + private String servletName; + + private String beanName; + + + /** + * Set the name of the servlet to forward to, + * i.e. the "servlet-name" of the target servlet in web.xml. + *

Default is the bean name of this controller. + */ + public void setServletName(String servletName) { + this.servletName = servletName; + } + + public void setBeanName(String name) { + this.beanName = name; + if (this.servletName == null) { + this.servletName = name; + } + } + + + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) + throws Exception { + + RequestDispatcher rd = getServletContext().getNamedDispatcher(this.servletName); + if (rd == null) { + throw new ServletException("No servlet with name '" + this.servletName + "' defined in web.xml"); + } + // If already included, include again, else forward. + if (useInclude(request, response)) { + rd.include(request, response); + if (logger.isDebugEnabled()) { + logger.debug("Included servlet [" + this.servletName + + "] in ServletForwardingController '" + this.beanName + "'"); + } + } + else { + rd.forward(request, response); + if (logger.isDebugEnabled()) { + logger.debug("Forwarded to servlet [" + this.servletName + + "] in ServletForwardingController '" + this.beanName + "'"); + } + } + return null; + } + + /** + * Determine whether to use RequestDispatcher's include or + * forward method. + *

Performs a check whether an include URI attribute is found in the request, + * indicating an include request, and whether the response has already been committed. + * In both cases, an include will be performed, as a forward is not possible anymore. + * @param request current HTTP request + * @param response current HTTP response + * @return true for include, false for forward + * @see javax.servlet.RequestDispatcher#forward + * @see javax.servlet.RequestDispatcher#include + * @see javax.servlet.ServletResponse#isCommitted + * @see org.springframework.web.util.WebUtils#isIncludeRequest + */ + protected boolean useInclude(HttpServletRequest request, HttpServletResponse response) { + return (WebUtils.isIncludeRequest(request) || response.isCommitted()); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/ServletWrappingController.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/ServletWrappingController.java new file mode 100644 index 00000000000..7d94e9f0490 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/ServletWrappingController.java @@ -0,0 +1,196 @@ +/* + * Copyright 2002-2007 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.web.servlet.mvc; + +import java.util.Enumeration; +import java.util.Properties; + +import javax.servlet.Servlet; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.web.servlet.ModelAndView; + +/** + * Spring Controller implementation that wraps a servlet instance which it manages + * internally. Such a wrapped servlet is not known outside of this controller; + * its entire lifecycle is covered here (in contrast to {@link ServletForwardingController}). + * + *

Useful to invoke an existing servlet via Spring's dispatching infrastructure, + * for example to apply Spring HandlerInterceptors to its requests. + * + *

Note that Struts has a special requirement in that it parses web.xml + * to find its servlet mapping. Therefore, you need to specify the DispatcherServlet's + * servlet name as "servletName" on this controller, so that Struts finds the + * DispatcherServlet's mapping (thinking that it refers to the ActionServlet). + * + *

Example: a DispatcherServlet XML context, forwarding "*.do" to the Struts + * ActionServlet wrapped by a ServletWrappingController. All such requests will go + * through the configured HandlerInterceptor chain (e.g. an OpenSessionInViewInterceptor). + * From the Struts point of view, everything will work as usual. + * + *

+ * <bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
+ *   <property name="interceptors">
+ *     <list>
+ *       <ref bean="openSessionInViewInterceptor"/>
+ *     </list>
+ *   </property>
+ *   <property name="mappings">
+ *     <props>
+ *       <prop key="*.do">strutsWrappingController</prop>
+ *     </props>
+ *   </property>
+ * </bean>
+ *
+ * <bean id="strutsWrappingController" class="org.springframework.web.servlet.mvc.ServletWrappingController">
+ *   <property name="servletClass">
+ *     <value>org.apache.struts.action.ActionServlet</value>
+ *   </property>
+ *   <property name="servletName">
+ *     <value>action</value>
+ *   </property>
+ *   <property name="initParameters">
+ *     <props>
+ *       <prop key="config">/WEB-INF/struts-config.xml</prop>
+ *     </props>
+ *   </property>
+ * </bean>
+ * + * @author Juergen Hoeller + * @since 1.1.1 + * @see ServletForwardingController + * @see org.springframework.orm.hibernate3.support.OpenSessionInViewInterceptor + * @see org.springframework.orm.hibernate3.support.OpenSessionInViewFilter + * @see org.springframework.orm.jdo.support.OpenPersistenceManagerInViewInterceptor + * @see org.springframework.orm.jdo.support.OpenPersistenceManagerInViewFilter + */ +public class ServletWrappingController extends AbstractController + implements BeanNameAware, InitializingBean, DisposableBean { + + private Class servletClass; + + private String servletName; + + private Properties initParameters = new Properties(); + + private String beanName; + + private Servlet servletInstance; + + + /** + * Set the class of the servlet to wrap. + * Needs to implement javax.servlet.Servlet. + * @see javax.servlet.Servlet + */ + public void setServletClass(Class servletClass) { + this.servletClass = servletClass; + } + + /** + * Set the name of the servlet to wrap. + * Default is the bean name of this controller. + */ + public void setServletName(String servletName) { + this.servletName = servletName; + } + + /** + * Specify init parameters for the servlet to wrap, + * as name-value pairs. + */ + public void setInitParameters(Properties initParameters) { + this.initParameters = initParameters; + } + + public void setBeanName(String name) { + this.beanName = name; + } + + + /** + * Initialize the wrapped Servlet instance. + * @see javax.servlet.Servlet#init(javax.servlet.ServletConfig) + */ + public void afterPropertiesSet() throws Exception { + if (this.servletClass == null) { + throw new IllegalArgumentException("servletClass is required"); + } + if (!Servlet.class.isAssignableFrom(this.servletClass)) { + throw new IllegalArgumentException("servletClass [" + this.servletClass.getName() + + "] needs to implement interface [javax.servlet.Servlet]"); + } + if (this.servletName == null) { + this.servletName = this.beanName; + } + this.servletInstance = (Servlet) this.servletClass.newInstance(); + this.servletInstance.init(new DelegatingServletConfig()); + } + + + /** + * Invoke the the wrapped Servlet instance. + * @see javax.servlet.Servlet#service(javax.servlet.ServletRequest, javax.servlet.ServletResponse) + */ + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) + throws Exception { + + this.servletInstance.service(request, response); + return null; + } + + + /** + * Destroy the wrapped Servlet instance. + * @see javax.servlet.Servlet#destroy() + */ + public void destroy() { + this.servletInstance.destroy(); + } + + + /** + * Internal implementation of the ServletConfig interface, to be passed + * to the wrapped servlet. Delegates to ServletWrappingController fields + * and methods to provide init parameters and other environment info. + */ + private class DelegatingServletConfig implements ServletConfig { + + public String getServletName() { + return servletName; + } + + public ServletContext getServletContext() { + return ServletWrappingController.this.getServletContext(); + } + + public String getInitParameter(String paramName) { + return initParameters.getProperty(paramName); + } + + public Enumeration getInitParameterNames() { + return initParameters.keys(); + } + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/SimpleControllerHandlerAdapter.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/SimpleControllerHandlerAdapter.java new file mode 100644 index 00000000000..6577e2997c9 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/SimpleControllerHandlerAdapter.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2007 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.web.servlet.mvc; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.web.servlet.HandlerAdapter; +import org.springframework.web.servlet.ModelAndView; + +/** + * Adapter to use the plain {@link Controller} workflow interface with + * the generic {@link org.springframework.web.servlet.DispatcherServlet}. + * Supports handlers that implement the {@link LastModified} interface. + * + *

This is an SPI class, not used directly by application code. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see org.springframework.web.servlet.DispatcherServlet + * @see Controller + * @see LastModified + * @see HttpRequestHandlerAdapter + */ +public class SimpleControllerHandlerAdapter implements HandlerAdapter { + + public boolean supports(Object handler) { + return (handler instanceof Controller); + } + + public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + + return ((Controller) handler).handleRequest(request, response); + } + + public long getLastModified(HttpServletRequest request, Object handler) { + if (handler instanceof LastModified) { + return ((LastModified) handler).getLastModified(request); + } + return -1L; + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/SimpleFormController.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/SimpleFormController.java new file mode 100644 index 00000000000..b512b1fcb2f --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/SimpleFormController.java @@ -0,0 +1,462 @@ +/* + * Copyright 2002-2007 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.web.servlet.mvc; + +import java.util.Map; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.validation.BindException; +import org.springframework.validation.Errors; +import org.springframework.web.servlet.ModelAndView; + +/** + *

Concrete FormController implementation that provides configurable + * form and success views, and an onSubmit chain for convenient overriding. + * Automatically resubmits to the form view in case of validation errors, + * and renders the success view in case of a valid submission.

+ * + *

The workflow of this Controller does not differ much from the one described + * in the {@link AbstractFormController AbstractFormController}. The difference + * is that you do not need to implement {@link #showForm showForm} and + * {@link #processFormSubmission processFormSubmission}: A form view and a + * success view can be configured declaratively.

+ * + *

Workflow + * (in addition to the superclass):
+ *

    + *
  1. Call to {@link #processFormSubmission processFormSubmission} which inspects + * the {@link org.springframework.validation.Errors Errors} object to see if + * any errors have occurred during binding and validation.
  2. + *
  3. If errors occured, the controller will return the configured formView, + * showing the form again (possibly rendering according error messages).
  4. + *
  5. If {@link #isFormChangeRequest isFormChangeRequest} is overridden and returns + * true for the given request, the controller will return the formView too. + * In that case, the controller will also suppress validation. Before returning the formView, + * the controller will invoke {@link #onFormChange}, giving sub-classes a chance + * to make modification to the command object. + * This is intended for requests that change the structure of the form, + * which should not cause validation and show the form in any case.
  6. + *
  7. If no errors occurred, the controller will call + * {@link #onSubmit(HttpServletRequest, HttpServletResponse, Object, BindException) onSubmit} + * using all parameters, which in case of the default implementation delegates to + * {@link #onSubmit(Object, BindException) onSubmit} with just the command object. + * The default implementation of the latter method will return the configured + * successView. Consider implementing {@link #doSubmitAction} doSubmitAction + * for simply performing a submit action and rendering the success view.
  8. + *
+ *

+ * + *

The submit behavior can be customized by overriding one of the + * {@link #onSubmit onSubmit} methods. Submit actions can also perform + * custom validation if necessary (typically database-driven checks), calling + * {@link #showForm(HttpServletRequest, HttpServletResponse, BindException) showForm} + * in case of validation errors to show the form view again.

+ * + *

Exposed configuration properties + * (and those defined by superclass):
+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
namedefaultdescription
formViewnullIndicates what view to use when the user asks for a new form + * or when validation errors have occurred on form submission.
successViewnullIndicates what view to use when successful form submissions have + * occurred. Such a success view could e.g. display a submission summary. + * More sophisticated actions can be implemented by overriding one of + * the {@link #onSubmit(Object) onSubmit()} methods.
+ *

+ * + * @author Juergen Hoeller + * @author Rob Harrop + * @since 05.05.2003 + */ +public class SimpleFormController extends AbstractFormController { + + private String formView; + + private String successView; + + + /** + * Create a new SimpleFormController. + *

Subclasses should set the following properties, either in the constructor + * or via a BeanFactory: commandName, commandClass, sessionForm, formView, + * successView. Note that commandClass doesn't need to be set when overriding + * formBackingObject, as this determines the class anyway. + * @see #setCommandClass + * @see #setCommandName + * @see #setSessionForm + * @see #setFormView + * @see #setSuccessView + * @see #formBackingObject + */ + public SimpleFormController() { + // AbstractFormController sets default cache seconds to 0. + super(); + } + + /** + * Set the name of the view that should be used for form display. + */ + public final void setFormView(String formView) { + this.formView = formView; + } + + /** + * Return the name of the view that should be used for form display. + */ + public final String getFormView() { + return this.formView; + } + + /** + * Set the name of the view that should be shown on successful submit. + */ + public final void setSuccessView(String successView) { + this.successView = successView; + } + + /** + * Return the name of the view that should be shown on successful submit. + */ + public final String getSuccessView() { + return this.successView; + } + + + /** + * This implementation shows the configured form view, delegating to the analogous + * {@link #showForm(HttpServletRequest, HttpServletResponse, BindException, Map)} + * variant with a "controlModel" argument. + *

Can be called within + * {@link #onSubmit(HttpServletRequest, HttpServletResponse, Object, BindException)} + * implementations, to redirect back to the form in case of custom validation errors + * (errors not determined by the validator). + *

Can be overridden in subclasses to show a custom view, writing directly + * to the response or preparing the response before rendering a view. + *

If calling showForm with a custom control model in subclasses, it's preferable + * to override the analogous showForm version with a controlModel argument + * (which will handle both standard form showing and custom form showing then). + * @see #setFormView + * @see #showForm(HttpServletRequest, HttpServletResponse, BindException, Map) + */ + protected ModelAndView showForm( + HttpServletRequest request, HttpServletResponse response, BindException errors) + throws Exception { + + return showForm(request, response, errors, null); + } + + /** + * This implementation shows the configured form view. + *

Can be called within + * {@link #onSubmit(HttpServletRequest, HttpServletResponse, Object, BindException)} + * implementations, to redirect back to the form in case of custom validation errors + * (errors not determined by the validator). + *

Can be overridden in subclasses to show a custom view, writing directly + * to the response or preparing the response before rendering a view. + * @param request current HTTP request + * @param errors validation errors holder + * @param controlModel model map containing controller-specific control data + * (e.g. current page in wizard-style controllers or special error message) + * @return the prepared form view + * @throws Exception in case of invalid state or arguments + * @see #setFormView + */ + protected ModelAndView showForm( + HttpServletRequest request, HttpServletResponse response, BindException errors, Map controlModel) + throws Exception { + + return showForm(request, errors, getFormView(), controlModel); + } + + /** + * Create a reference data map for the given request and command, + * consisting of bean name/bean instance pairs as expected by ModelAndView. + *

The default implementation delegates to {@link #referenceData(HttpServletRequest)}. + * Subclasses can override this to set reference data used in the view. + * @param request current HTTP request + * @param command form object with request parameters bound onto it + * @param errors validation errors holder + * @return a Map with reference data entries, or null if none + * @throws Exception in case of invalid state or arguments + * @see ModelAndView + */ + protected Map referenceData(HttpServletRequest request, Object command, Errors errors) throws Exception { + return referenceData(request); + } + + /** + * Create a reference data map for the given request. + * Called by the {@link #referenceData(HttpServletRequest, Object, Errors)} + * variant with all parameters. + *

The default implementation returns null. + * Subclasses can override this to set reference data used in the view. + * @param request current HTTP request + * @return a Map with reference data entries, or null if none + * @throws Exception in case of invalid state or arguments + * @see #referenceData(HttpServletRequest, Object, Errors) + * @see ModelAndView + */ + protected Map referenceData(HttpServletRequest request) throws Exception { + return null; + } + + + /** + * This implementation calls + * {@link #showForm(HttpServletRequest, HttpServletResponse, BindException)} + * in case of errors, and delegates to the full + * {@link #onSubmit(HttpServletRequest, HttpServletResponse, Object, BindException)}'s + * variant else. + *

This can only be overridden to check for an action that should be executed + * without respect to binding errors, like a cancel action. To just handle successful + * submissions without binding errors, override one of the onSubmit + * methods or {@link #doSubmitAction}. + * @see #showForm(HttpServletRequest, HttpServletResponse, BindException) + * @see #onSubmit(HttpServletRequest, HttpServletResponse, Object, BindException) + * @see #onSubmit(Object, BindException) + * @see #onSubmit(Object) + * @see #doSubmitAction(Object) + */ + protected ModelAndView processFormSubmission( + HttpServletRequest request, HttpServletResponse response, Object command, BindException errors) + throws Exception { + + if (errors.hasErrors()) { + if (logger.isDebugEnabled()) { + logger.debug("Data binding errors: " + errors.getErrorCount()); + } + return showForm(request, response, errors); + } + else if (isFormChangeRequest(request, command)) { + logger.debug("Detected form change request -> routing request to onFormChange"); + onFormChange(request, response, command, errors); + return showForm(request, response, errors); + } + else { + logger.debug("No errors -> processing submit"); + return onSubmit(request, response, command, errors); + } + } + + /** + * This implementation delegates to {@link #isFormChangeRequest(HttpServletRequest, Object)}: + * A form change request changes the appearance of the form and should not get + * validated but just show the new form. + * @see #isFormChangeRequest + */ + protected boolean suppressValidation(HttpServletRequest request, Object command) { + return isFormChangeRequest(request, command); + } + + /** + * Determine whether the given request is a form change request. + * A form change request changes the appearance of the form + * and should always show the new form, without validation. + *

Gets called by {@link #suppressValidation} and {@link #processFormSubmission}. + * Consequently, this single method determines to suppress validation + * and to show the form view in any case. + *

The default implementation delegates to + * {@link #isFormChangeRequest(javax.servlet.http.HttpServletRequest)}. + * @param request current HTTP request + * @param command form object with request parameters bound onto it + * @return whether the given request is a form change request + * @see #suppressValidation + * @see #processFormSubmission + */ + protected boolean isFormChangeRequest(HttpServletRequest request, Object command) { + return isFormChangeRequest(request); + } + + /** + * Simpler isFormChangeRequest variant, called by the full + * variant {@link #isFormChangeRequest(HttpServletRequest, Object)}. + *

The default implementation returns falsetrue. Allows subclasses to implement custom logic + * to modify the command object to directly modify data in the form. + *

The default implementation delegates to + * {@link #onFormChange(HttpServletRequest, HttpServletResponse, Object, BindException)}. + * @param request current servlet request + * @param response current servlet response + * @param command form object with request parameters bound onto it + * @param errors validation errors holder, allowing for additional + * custom validation + * @throws Exception in case of errors + * @see #isFormChangeRequest(HttpServletRequest) + * @see #onFormChange(HttpServletRequest, HttpServletResponse, Object) + */ + protected void onFormChange( + HttpServletRequest request, HttpServletResponse response, Object command, BindException errors) + throws Exception { + + onFormChange(request, response, command); + } + + /** + * Simpler onFormChange variant, called by the full variant + * {@link #onFormChange(HttpServletRequest, HttpServletResponse, Object, BindException)}. + *

The default implementation is empty. + * @param request current servlet request + * @param response current servlet response + * @param command form object with request parameters bound onto it + * @throws Exception in case of errors + * @see #onFormChange(HttpServletRequest, HttpServletResponse, Object, BindException) + */ + protected void onFormChange(HttpServletRequest request, HttpServletResponse response, Object command) + throws Exception { + } + + + /** + * Submit callback with all parameters. Called in case of submit without errors + * reported by the registered validator, or on every submit if no validator. + *

The default implementation delegates to {@link #onSubmit(Object, BindException)}. + * For simply performing a submit action and rendering the specified success + * view, consider implementing {@link #doSubmitAction} rather than an + * onSubmit variant. + *

Subclasses can override this to provide custom submission handling like storing + * the object to the database. Implementations can also perform custom validation and + * call showForm to return to the form. Do not implement multiple onSubmit + * methods: In that case, just this method will be called by the controller. + *

Call errors.getModel() to populate the ModelAndView model + * with the command and the Errors instance, under the specified command name, + * as expected by the "spring:bind" tag. + * @param request current servlet request + * @param response current servlet response + * @param command form object with request parameters bound onto it + * @param errors Errors instance without errors (subclass can add errors if it wants to) + * @return the prepared model and view, or null + * @throws Exception in case of errors + * @see #onSubmit(Object, BindException) + * @see #doSubmitAction + * @see #showForm + * @see org.springframework.validation.Errors + * @see org.springframework.validation.BindException#getModel + */ + protected ModelAndView onSubmit( + HttpServletRequest request, HttpServletResponse response, Object command, BindException errors) + throws Exception { + + return onSubmit(command, errors); + } + + /** + * Simpler onSubmit variant. + * Called by the default implementation of the + * {@link #onSubmit(HttpServletRequest, HttpServletResponse, Object, BindException)} + * variant with all parameters. + *

The default implementation calls {@link #onSubmit(Object)}, using the + * returned ModelAndView if actually implemented in a subclass. Else, the + * default behavior will apply: rendering the success view with the command + * and Errors instance as model. + *

Subclasses can override this to provide custom submission handling that + * does not need request and response. + *

Call errors.getModel() to populate the ModelAndView model + * with the command and the Errors instance, under the specified command name, + * as expected by the "spring:bind" tag. + * @param command form object with request parameters bound onto it + * @param errors Errors instance without errors + * @return the prepared model and view + * @throws Exception in case of errors + * @see #onSubmit(HttpServletRequest, HttpServletResponse, Object, BindException) + * @see #onSubmit(Object) + * @see #setSuccessView + * @see org.springframework.validation.Errors + * @see org.springframework.validation.BindException#getModel + */ + protected ModelAndView onSubmit(Object command, BindException errors) throws Exception { + ModelAndView mv = onSubmit(command); + if (mv != null) { + // simplest onSubmit variant implemented in custom subclass + return mv; + } + else { + // default behavior: render success view + if (getSuccessView() == null) { + throw new ServletException("successView isn't set"); + } + return new ModelAndView(getSuccessView(), errors.getModel()); + } + } + + /** + * Simplest onSubmit variant. Called by the default implementation + * of the {@link #onSubmit(Object, BindException)} variant. + *

This implementation calls {@link #doSubmitAction(Object)} and returns + * null as ModelAndView, making the calling onSubmit + * method perform its default rendering of the success view. + *

Subclasses can override this to provide custom submission handling + * that just depends on the command object. It's preferable to use either + * {@link #onSubmit(Object, BindException)} or {@link #doSubmitAction(Object)}, + * though: Use the former when you want to build your own ModelAndView; use the + * latter when you want to perform an action and forward to the successView. + * @param command form object with request parameters bound onto it + * @return the prepared model and view, or null for default + * (that is, rendering the configured "successView") + * @throws Exception in case of errors + * @see #onSubmit(Object, BindException) + * @see #doSubmitAction + * @see #setSuccessView + */ + protected ModelAndView onSubmit(Object command) throws Exception { + doSubmitAction(command); + return null; + } + + /** + * Template method for submit actions. Called by the default implementation + * of the simplest {@link #onSubmit(Object)} variant. + *

This is the preferred submit callback to implement if you want to + * perform an action (like storing changes to the database) and then render + * the success view with the command and Errors instance as model. + * You don't need to care about the success ModelAndView here. + * @param command form object with request parameters bound onto it + * @throws Exception in case of errors + * @see #onSubmit(Object) + * @see #setSuccessView + */ + protected void doSubmitAction(Object command) throws Exception { + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/UrlFilenameViewController.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/UrlFilenameViewController.java new file mode 100644 index 00000000000..ae569c2ff47 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/UrlFilenameViewController.java @@ -0,0 +1,163 @@ +/* + * Copyright 2002-2007 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.web.servlet.mvc; + +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.core.CollectionFactory; +import org.springframework.util.StringUtils; +import org.springframework.web.servlet.HandlerMapping; + +/** + * Simple Controller implementation that transforms the virtual + * path of a URL into a view name and returns that view. + * + *

Can optionally prepend a {@link #setPrefix prefix} and/or append a + * {@link #setSuffix suffix} to build the viewname from the URL filename. + * + *

Find below some examples: + * + *

    + *
  1. "/index" -> "index"
  2. + *
  3. "/index.html" -> "index"
  4. + *
  5. "/index.html" + prefix "pre_" and suffix "_suf" -> "pre_index_suf"
  6. + *
  7. "/products/view.html" -> "products/view"
  8. + *
+ * + *

Thanks to David Barri for suggesting prefix/suffix support! + * + * @author Alef Arendsen + * @author Juergen Hoeller + * @author Rob Harrop + * @see #setPrefix + * @see #setSuffix + */ +public class UrlFilenameViewController extends AbstractUrlViewController { + + private String prefix = ""; + + private String suffix = ""; + + /** Request URL path String --> view name String */ + private final Map viewNameCache = CollectionFactory.createConcurrentMapIfPossible(16); + + + /** + * Set the prefix to prepend to the request URL filename + * to build a view name. + */ + public void setPrefix(String prefix) { + this.prefix = (prefix != null ? prefix : ""); + } + + /** + * Return the prefix to prepend to the request URL filename. + */ + protected String getPrefix() { + return this.prefix; + } + + /** + * Set the suffix to append to the request URL filename + * to build a view name. + */ + public void setSuffix(String suffix) { + this.suffix = (suffix != null ? suffix : ""); + } + + /** + * Return the suffix to append to the request URL filename. + */ + protected String getSuffix() { + return this.suffix; + } + + + /** + * Returns view name based on the URL filename, + * with prefix/suffix applied when appropriate. + * @see #extractViewNameFromUrlPath + * @see #setPrefix + * @see #setSuffix + */ + protected String getViewNameForRequest(HttpServletRequest request) { + String uri = extractOperableUrl(request); + return getViewNameForUrlPath(uri); + } + + /** + * Extract a URL path from the given request, + * suitable for view name extraction. + * @param request current HTTP request + * @return the URL to use for view name extraction + */ + protected String extractOperableUrl(HttpServletRequest request) { + String urlPath = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE); + if (!StringUtils.hasText(urlPath)) { + urlPath = getUrlPathHelper().getLookupPathForRequest(request); + } + return urlPath; + } + + /** + * Returns view name based on the URL filename, + * with prefix/suffix applied when appropriate. + * @param uri the request URI; for example "/index.html" + * @return the extracted URI filename; for example "index" + * @see #extractViewNameFromUrlPath + * @see #postProcessViewName + */ + protected String getViewNameForUrlPath(String uri) { + String viewName = (String) this.viewNameCache.get(uri); + if (viewName == null) { + viewName = extractViewNameFromUrlPath(uri); + viewName = postProcessViewName(viewName); + this.viewNameCache.put(uri, viewName); + } + return viewName; + } + + /** + * Extract the URL filename from the given request URI. + * @param uri the request URI; for example "/index.html" + * @return the extracted URI filename; for example "index" + */ + protected String extractViewNameFromUrlPath(String uri) { + int start = (uri.charAt(0) == '/' ? 1 : 0); + int lastIndex = uri.lastIndexOf("."); + int end = (lastIndex < 0 ? uri.length() : lastIndex); + return uri.substring(start, end); + } + + /** + * Build the full view name based on the given view name + * as indicated by the URL path. + *

The default implementation simply applies prefix and suffix. + * This can be overridden, for example, to manipulate upper case + * / lower case, etc. + * @param viewName the original view name, as indicated by the URL path + * @return the full view name to use + * @see #getPrefix() + * @see #getSuffix() + */ + protected String postProcessViewName(String viewName) { + return getPrefix() + viewName + getSuffix(); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/WebContentInterceptor.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/WebContentInterceptor.java new file mode 100644 index 00000000000..5d6fc1e9611 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/WebContentInterceptor.java @@ -0,0 +1,199 @@ +/* + * Copyright 2002-2007 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.web.servlet.mvc; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Properties; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.util.AntPathMatcher; +import org.springframework.util.Assert; +import org.springframework.util.PathMatcher; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.support.WebContentGenerator; +import org.springframework.web.util.UrlPathHelper; + +/** + * Interceptor that checks and prepares request and response. Checks for supported + * methods and a required session, and applies the specified number of cache seconds. + * See superclass bean properties for configuration options. + * + *

All the settings supported by this interceptor can also be set on AbstractController. + * This interceptor is mainly intended for applying checks and preparations to a set of + * controllers mapped by a HandlerMapping. + * + * @author Juergen Hoeller + * @since 27.11.2003 + * @see AbstractController + */ +public class WebContentInterceptor extends WebContentGenerator implements HandlerInterceptor { + + private UrlPathHelper urlPathHelper = new UrlPathHelper(); + + private Map cacheMappings = new HashMap(); + + private PathMatcher pathMatcher = new AntPathMatcher(); + + + /** + * Set if URL lookup should always use full path within current servlet + * context. Else, the path within the current servlet mapping is used + * if applicable (i.e. in the case of a ".../*" servlet mapping in web.xml). + * Default is "false". + *

Only relevant for the "cacheMappings" setting. + * @see #setCacheMappings + * @see org.springframework.web.util.UrlPathHelper#setAlwaysUseFullPath + */ + public void setAlwaysUseFullPath(boolean alwaysUseFullPath) { + this.urlPathHelper.setAlwaysUseFullPath(alwaysUseFullPath); + } + + /** + * Set if context path and request URI should be URL-decoded. + * Both are returned undecoded by the Servlet API, + * in contrast to the servlet path. + *

Uses either the request encoding or the default encoding according + * to the Servlet spec (ISO-8859-1). + *

Only relevant for the "cacheMappings" setting. + * @see #setCacheMappings + * @see org.springframework.web.util.UrlPathHelper#setUrlDecode + */ + public void setUrlDecode(boolean urlDecode) { + this.urlPathHelper.setUrlDecode(urlDecode); + } + + /** + * Set the UrlPathHelper to use for resolution of lookup paths. + *

Use this to override the default UrlPathHelper with a custom subclass, + * or to share common UrlPathHelper settings across multiple HandlerMappings + * and MethodNameResolvers. + *

Only relevant for the "cacheMappings" setting. + * @see #setCacheMappings + * @see org.springframework.web.servlet.handler.AbstractUrlHandlerMapping#setUrlPathHelper + * @see org.springframework.web.servlet.mvc.multiaction.AbstractUrlMethodNameResolver#setUrlPathHelper + */ + public void setUrlPathHelper(UrlPathHelper urlPathHelper) { + Assert.notNull(urlPathHelper, "UrlPathHelper must not be null"); + this.urlPathHelper = urlPathHelper; + } + + /** + * Map specific URL paths to specific cache seconds. + *

Overrides the default cache seconds setting of this interceptor. + * Can specify "-1" to exclude a URL path from default caching. + *

Supports direct matches, e.g. a registered "/test" matches "/test", + * and a various Ant-style pattern matches, e.g. a registered "/t*" matches + * both "/test" and "/team". For details, see the AntPathMatcher javadoc. + * @param cacheMappings a mapping between URL paths (as keys) and + * cache seconds (as values, need to be integer-parsable) + * @see #setCacheSeconds + * @see org.springframework.util.AntPathMatcher + */ + public void setCacheMappings(Properties cacheMappings) { + this.cacheMappings.clear(); + for (Iterator it = cacheMappings.keySet().iterator(); it.hasNext();) { + String path = (String) it.next(); + this.cacheMappings.put(path, Integer.valueOf(cacheMappings.getProperty(path))); + } + } + + /** + * Set the PathMatcher implementation to use for matching URL paths + * against registered URL patterns, for determining cache mappings. + * Default is AntPathMatcher. + * @see #setCacheMappings + * @see org.springframework.util.AntPathMatcher + */ + public void setPathMatcher(PathMatcher pathMatcher) { + Assert.notNull(pathMatcher, "PathMatcher must not be null"); + this.pathMatcher = pathMatcher; + } + + + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws ServletException { + + String lookupPath = this.urlPathHelper.getLookupPathForRequest(request); + if (logger.isDebugEnabled()) { + logger.debug("Looking up cache seconds for [" + lookupPath + "]"); + } + + Integer cacheSeconds = lookupCacheSeconds(lookupPath); + if (cacheSeconds != null) { + if (logger.isDebugEnabled()) { + logger.debug("Applying " + cacheSeconds + " cache seconds to [" + lookupPath + "]"); + } + checkAndPrepare(request, response, cacheSeconds.intValue(), handler instanceof LastModified); + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Applying default cache seconds to [" + lookupPath + "]"); + } + checkAndPrepare(request, response, handler instanceof LastModified); + } + + return true; + } + + /** + * Look up a cache seconds value for the given URL path. + *

Supports direct matches, e.g. a registered "/test" matches "/test", + * and various Ant-style pattern matches, e.g. a registered "/t*" matches + * both "/test" and "/team". For details, see the AntPathMatcher class. + * @param urlPath URL the bean is mapped to + * @return the associated cache seconds, or null if not found + * @see org.springframework.util.AntPathMatcher + */ + protected Integer lookupCacheSeconds(String urlPath) { + // direct match? + Integer cacheSeconds = (Integer) this.cacheMappings.get(urlPath); + if (cacheSeconds == null) { + // pattern match? + for (Iterator it = this.cacheMappings.keySet().iterator(); it.hasNext();) { + String registeredPath = (String) it.next(); + if (this.pathMatcher.match(registeredPath, urlPath)) { + cacheSeconds = (Integer) this.cacheMappings.get(registeredPath); + } + } + } + return cacheSeconds; + } + + + /** + * This implementation is empty. + */ + public void postHandle( + HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) + throws Exception { + } + + /** + * This implementation is empty. + */ + public void afterCompletion( + HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) + throws Exception { + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java new file mode 100644 index 00000000000..2867a72d879 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java @@ -0,0 +1,681 @@ +/* + * Copyright 2002-2008 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.web.servlet.mvc.annotation; + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.Writer; +import java.lang.reflect.Method; +import java.security.Principal; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.BeanUtils; +import org.springframework.core.Conventions; +import org.springframework.core.GenericTypeResolver; +import org.springframework.core.LocalVariableTableParameterNameDiscoverer; +import org.springframework.core.MethodParameter; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.ui.ExtendedModelMap; +import org.springframework.ui.Model; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.PathMatcher; +import org.springframework.validation.support.BindingAwareModelMap; +import org.springframework.web.HttpSessionRequiredException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.ServletRequestDataBinder; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.InitBinder; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.SessionAttributes; +import org.springframework.web.bind.annotation.support.HandlerMethodInvoker; +import org.springframework.web.bind.annotation.support.HandlerMethodResolver; +import org.springframework.web.bind.support.DefaultSessionAttributeStore; +import org.springframework.web.bind.support.SessionAttributeStore; +import org.springframework.web.bind.support.WebArgumentResolver; +import org.springframework.web.bind.support.WebBindingInitializer; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.servlet.HandlerAdapter; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.View; +import org.springframework.web.servlet.mvc.multiaction.InternalPathMethodNameResolver; +import org.springframework.web.servlet.mvc.multiaction.MethodNameResolver; +import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException; +import org.springframework.web.servlet.support.RequestContextUtils; +import org.springframework.web.servlet.support.WebContentGenerator; +import org.springframework.web.util.UrlPathHelper; +import org.springframework.web.util.WebUtils; + +/** + * Implementation of the {@link org.springframework.web.servlet.HandlerAdapter} + * interface that maps handler methods based on HTTP paths, HTTP methods and + * request parameters expressed through the {@link RequestMapping} annotation. + * + *

Supports request parameter binding through the {@link RequestParam} annotation. + * Also supports the {@link ModelAttribute} annotation for exposing model attribute + * values to the view, as well as {@link InitBinder} for binder initialization methods + * and {@link SessionAttributes} for automatic session management of specific attributes. + * + *

This adapter can be customized through various bean properties. + * A common use case is to apply shared binder initialization logic through + * a custom {@link #setWebBindingInitializer WebBindingInitializer}. + * + * @author Juergen Hoeller + * @author Arjen Poutsma + * @since 2.5 + * @see #setPathMatcher + * @see #setMethodNameResolver + * @see #setWebBindingInitializer + * @see #setSessionAttributeStore + */ +public class AnnotationMethodHandlerAdapter extends WebContentGenerator implements HandlerAdapter { + + /** + * Log category to use when no mapped handler is found for a request. + * @see #pageNotFoundLogger + */ + public static final String PAGE_NOT_FOUND_LOG_CATEGORY = "org.springframework.web.servlet.PageNotFound"; + + + /** + * Additional logger to use when no mapped handler is found for a request. + * @see #PAGE_NOT_FOUND_LOG_CATEGORY + */ + protected static final Log pageNotFoundLogger = LogFactory.getLog(PAGE_NOT_FOUND_LOG_CATEGORY); + + private UrlPathHelper urlPathHelper = new UrlPathHelper(); + + private PathMatcher pathMatcher = new AntPathMatcher(); + + private MethodNameResolver methodNameResolver = new InternalPathMethodNameResolver(); + + private WebBindingInitializer webBindingInitializer; + + private SessionAttributeStore sessionAttributeStore = new DefaultSessionAttributeStore(); + + private int cacheSecondsForSessionAttributeHandlers = 0; + + private boolean synchronizeOnSession = false; + + private ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer(); + + private WebArgumentResolver[] customArgumentResolvers; + + private final Map, ServletHandlerMethodResolver> methodResolverCache = + new ConcurrentHashMap, ServletHandlerMethodResolver>(); + + + public AnnotationMethodHandlerAdapter() { + // no restriction of HTTP methods by default + super(false); + } + + + /** + * Set if URL lookup should always use the full path within the current servlet + * context. Else, the path within the current servlet mapping is used if applicable + * (that is, in the case of a ".../*" servlet mapping in web.xml). + *

Default is "false". + * @see org.springframework.web.util.UrlPathHelper#setAlwaysUseFullPath + */ + public void setAlwaysUseFullPath(boolean alwaysUseFullPath) { + this.urlPathHelper.setAlwaysUseFullPath(alwaysUseFullPath); + } + + /** + * Set if context path and request URI should be URL-decoded. Both are returned + * undecoded by the Servlet API, in contrast to the servlet path. + *

Uses either the request encoding or the default encoding according + * to the Servlet spec (ISO-8859-1). + * @see org.springframework.web.util.UrlPathHelper#setUrlDecode + */ + public void setUrlDecode(boolean urlDecode) { + this.urlPathHelper.setUrlDecode(urlDecode); + } + + /** + * Set the UrlPathHelper to use for resolution of lookup paths. + *

Use this to override the default UrlPathHelper with a custom subclass, + * or to share common UrlPathHelper settings across multiple HandlerMappings + * and HandlerAdapters. + */ + public void setUrlPathHelper(UrlPathHelper urlPathHelper) { + Assert.notNull(urlPathHelper, "UrlPathHelper must not be null"); + this.urlPathHelper = urlPathHelper; + } + + /** + * Set the PathMatcher implementation to use for matching URL paths + * against registered URL patterns. Default is AntPathMatcher. + * @see org.springframework.util.AntPathMatcher + */ + public void setPathMatcher(PathMatcher pathMatcher) { + Assert.notNull(pathMatcher, "PathMatcher must not be null"); + this.pathMatcher = pathMatcher; + } + + /** + * Set the MethodNameResolver to use for resolving default handler methods + * (carrying an empty @RequestMapping annotation). + *

Will only kick in when the handler method cannot be resolved uniquely + * through the annotation metadata already. + */ + public void setMethodNameResolver(MethodNameResolver methodNameResolver) { + this.methodNameResolver = methodNameResolver; + } + + /** + * Specify a WebBindingInitializer which will apply pre-configured + * configuration to every DataBinder that this controller uses. + */ + public void setWebBindingInitializer(WebBindingInitializer webBindingInitializer) { + this.webBindingInitializer = webBindingInitializer; + } + + /** + * Specify the strategy to store session attributes with. + *

Default is {@link org.springframework.web.bind.support.DefaultSessionAttributeStore}, + * storing session attributes in the HttpSession, using the same + * attribute name as in the model. + */ + public void setSessionAttributeStore(SessionAttributeStore sessionAttributeStore) { + Assert.notNull(sessionAttributeStore, "SessionAttributeStore must not be null"); + this.sessionAttributeStore = sessionAttributeStore; + } + + /** + * Cache content produced by @SessionAttributes annotated handlers + * for the given number of seconds. Default is 0, preventing caching completely. + *

In contrast to the "cacheSeconds" property which will apply to all general + * handlers (but not to @SessionAttributes annotated handlers), this + * setting will apply to @SessionAttributes annotated handlers only. + * @see #setCacheSeconds + * @see org.springframework.web.bind.annotation.SessionAttributes + */ + public void setCacheSecondsForSessionAttributeHandlers(int cacheSecondsForSessionAttributeHandlers) { + this.cacheSecondsForSessionAttributeHandlers = cacheSecondsForSessionAttributeHandlers; + } + + /** + * Set if controller execution should be synchronized on the session, + * to serialize parallel invocations from the same client. + *

More specifically, the execution of each handler method will get + * synchronized if this flag is "true". The best available session mutex + * will be used for the synchronization; ideally, this will be a mutex + * exposed by HttpSessionMutexListener. + *

The session mutex is guaranteed to be the same object during + * the entire lifetime of the session, available under the key defined + * by the SESSION_MUTEX_ATTRIBUTE constant. It serves as a + * safe reference to synchronize on for locking on the current session. + *

In many cases, the HttpSession reference itself is a safe mutex + * as well, since it will always be the same object reference for the + * same active logical session. However, this is not guaranteed across + * different servlet containers; the only 100% safe way is a session mutex. + * @see org.springframework.web.util.HttpSessionMutexListener + * @see org.springframework.web.util.WebUtils#getSessionMutex(javax.servlet.http.HttpSession) + */ + public void setSynchronizeOnSession(boolean synchronizeOnSession) { + this.synchronizeOnSession = synchronizeOnSession; + } + + /** + * Set the ParameterNameDiscoverer to use for resolving method parameter + * names if needed (e.g. for default attribute names). + *

Default is a {@link org.springframework.core.LocalVariableTableParameterNameDiscoverer}. + */ + public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) { + this.parameterNameDiscoverer = parameterNameDiscoverer; + } + + /** + * Set a custom ArgumentResolvers to use for special method parameter types. + * Such a custom ArgumentResolver will kick in first, having a chance to + * resolve an argument value before the standard argument handling kicks in. + */ + public void setCustomArgumentResolver(WebArgumentResolver argumentResolver) { + this.customArgumentResolvers = new WebArgumentResolver[] {argumentResolver}; + } + + /** + * Set one or more custom ArgumentResolvers to use for special method + * parameter types. Any such custom ArgumentResolver will kick in first, + * having a chance to resolve an argument value before the standard + * argument handling kicks in. + */ + public void setCustomArgumentResolvers(WebArgumentResolver[] argumentResolvers) { + this.customArgumentResolvers = argumentResolvers; + } + + + public boolean supports(Object handler) { + return getMethodResolver(handler).hasHandlerMethods(); + } + + public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + + if (handler.getClass().getAnnotation(SessionAttributes.class) != null) { + // Always prevent caching in case of session attribute management. + checkAndPrepare(request, response, this.cacheSecondsForSessionAttributeHandlers, true); + // Prepare cached set of session attributes names. + } + else { + // Uses configured default cacheSeconds setting. + checkAndPrepare(request, response, true); + } + + // Execute invokeHandlerMethod in synchronized block if required. + if (this.synchronizeOnSession) { + HttpSession session = request.getSession(false); + if (session != null) { + Object mutex = WebUtils.getSessionMutex(session); + synchronized (mutex) { + return invokeHandlerMethod(request, response, handler); + } + } + } + + return invokeHandlerMethod(request, response, handler); + } + + protected ModelAndView invokeHandlerMethod( + HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + + try { + ServletHandlerMethodResolver methodResolver = getMethodResolver(handler); + Method handlerMethod = methodResolver.resolveHandlerMethod(request); + ServletHandlerMethodInvoker methodInvoker = new ServletHandlerMethodInvoker(methodResolver); + ServletWebRequest webRequest = new ServletWebRequest(request, response); + ExtendedModelMap implicitModel = new BindingAwareModelMap(); + + Object result = methodInvoker.invokeHandlerMethod(handlerMethod, handler, webRequest, implicitModel); + ModelAndView mav = + methodInvoker.getModelAndView(handlerMethod, handler.getClass(), result, implicitModel, webRequest); + methodInvoker.updateModelAttributes( + handler, (mav != null ? mav.getModel() : null), implicitModel, webRequest); + return mav; + } + catch (NoSuchRequestHandlingMethodException ex) { + return handleNoSuchRequestHandlingMethod(ex, request, response); + } + } + + public long getLastModified(HttpServletRequest request, Object handler) { + return -1; + } + + + /** + * Handle the case where no request handler method was found. + *

The default implementation logs a warning and sends an HTTP 404 error. + * Alternatively, a fallback view could be chosen, or the + * NoSuchRequestHandlingMethodException could be rethrown as-is. + * @param ex the NoSuchRequestHandlingMethodException to be handled + * @param request current HTTP request + * @param response current HTTP response + * @return a ModelAndView to render, or null if handled directly + * @throws Exception an Exception that should be thrown as result of the servlet request + */ + protected ModelAndView handleNoSuchRequestHandlingMethod( + NoSuchRequestHandlingMethodException ex, HttpServletRequest request, HttpServletResponse response) + throws Exception { + + pageNotFoundLogger.warn(ex.getMessage()); + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return null; + } + + /** + * Template method for creating a new ServletRequestDataBinder instance. + *

The default implementation creates a standard ServletRequestDataBinder. + * This can be overridden for custom ServletRequestDataBinder subclasses. + * @param request current HTTP request + * @param target the target object to bind onto (or null + * if the binder is just used to convert a plain parameter value) + * @param objectName the objectName of the target object + * @return the ServletRequestDataBinder instance to use + * @throws Exception in case of invalid state or arguments + * @see ServletRequestDataBinder#bind(javax.servlet.ServletRequest) + * @see ServletRequestDataBinder#convertIfNecessary(Object, Class, MethodParameter) + */ + protected ServletRequestDataBinder createBinder( + HttpServletRequest request, Object target, String objectName) throws Exception { + + return new ServletRequestDataBinder(target, objectName); + } + + /** + * Build a HandlerMethodResolver for the given handler type. + */ + private ServletHandlerMethodResolver getMethodResolver(Object handler) { + Class handlerClass = ClassUtils.getUserClass(handler); + ServletHandlerMethodResolver resolver = this.methodResolverCache.get(handlerClass); + if (resolver == null) { + resolver = new ServletHandlerMethodResolver(handlerClass); + this.methodResolverCache.put(handlerClass, resolver); + } + return resolver; + } + + + private class ServletHandlerMethodResolver extends HandlerMethodResolver { + + public ServletHandlerMethodResolver(Class handlerType) { + super(handlerType); + } + + public Method resolveHandlerMethod(HttpServletRequest request) throws ServletException { + String lookupPath = urlPathHelper.getLookupPathForRequest(request); + Map targetHandlerMethods = new LinkedHashMap(); + Map targetPathMatches = new LinkedHashMap(); + String resolvedMethodName = null; + for (Method handlerMethod : getHandlerMethods()) { + RequestMappingInfo mappingInfo = new RequestMappingInfo(); + RequestMapping mapping = AnnotationUtils.findAnnotation(handlerMethod, RequestMapping.class); + mappingInfo.paths = mapping.value(); + if (!hasTypeLevelMapping() || !Arrays.equals(mapping.method(), getTypeLevelMapping().method())) { + mappingInfo.methods = mapping.method(); + } + if (!hasTypeLevelMapping() || !Arrays.equals(mapping.params(), getTypeLevelMapping().params())) { + mappingInfo.params = mapping.params(); + } + boolean match = false; + if (mappingInfo.paths.length > 0) { + for (String mappedPath : mappingInfo.paths) { + if (isPathMatch(mappedPath, lookupPath)) { + if (checkParameters(mappingInfo, request)) { + match = true; + targetPathMatches.put(mappingInfo, mappedPath); + } + else { + break; + } + } + } + } + else { + // No paths specified: parameter match sufficient. + match = checkParameters(mappingInfo, request); + if (match && mappingInfo.methods.length == 0 && mappingInfo.params.length == 0 && + resolvedMethodName != null && !resolvedMethodName.equals(handlerMethod.getName())) { + match = false; + } + } + if (match) { + Method oldMappedMethod = targetHandlerMethods.put(mappingInfo, handlerMethod); + if (oldMappedMethod != null && oldMappedMethod != handlerMethod) { + if (methodNameResolver != null && mappingInfo.paths.length == 0) { + if (!oldMappedMethod.getName().equals(handlerMethod.getName())) { + if (resolvedMethodName == null) { + resolvedMethodName = methodNameResolver.getHandlerMethodName(request); + } + if (!resolvedMethodName.equals(oldMappedMethod.getName())) { + oldMappedMethod = null; + } + if (!resolvedMethodName.equals(handlerMethod.getName())) { + if (oldMappedMethod != null) { + targetHandlerMethods.put(mappingInfo, oldMappedMethod); + oldMappedMethod = null; + } + else { + targetHandlerMethods.remove(mappingInfo); + } + } + } + } + if (oldMappedMethod != null) { + throw new IllegalStateException("Ambiguous handler methods mapped for HTTP path '" + + lookupPath + "': {" + oldMappedMethod + ", " + handlerMethod + + "}. If you intend to handle the same path in multiple methods, then factor " + + "them out into a dedicated handler class with that path mapped at the type level!"); + } + } + } + } + if (targetHandlerMethods.size() == 1) { + return targetHandlerMethods.values().iterator().next(); + } + else if (!targetHandlerMethods.isEmpty()) { + RequestMappingInfo bestMappingMatch = null; + String bestPathMatch = null; + for (RequestMappingInfo mapping : targetHandlerMethods.keySet()) { + String mappedPath = targetPathMatches.get(mapping); + if (bestMappingMatch == null) { + bestMappingMatch = mapping; + bestPathMatch = mappedPath; + } + else { + if (isBetterPathMatch(mappedPath, bestPathMatch, lookupPath) || + (!isBetterPathMatch(bestPathMatch, mappedPath, lookupPath) && + (isBetterMethodMatch(mapping, bestMappingMatch) || + (!isBetterMethodMatch(bestMappingMatch, mapping) && + isBetterParamMatch(mapping, bestMappingMatch))))) { + bestMappingMatch = mapping; + bestPathMatch = mappedPath; + } + } + } + return targetHandlerMethods.get(bestMappingMatch); + } + else { + throw new NoSuchRequestHandlingMethodException(lookupPath, request.getMethod(), request.getParameterMap()); + } + } + + private boolean isPathMatch(String mappedPath, String lookupPath) { + if (mappedPath.equals(lookupPath) || pathMatcher.match(mappedPath, lookupPath)) { + return true; + } + boolean hasSuffix = (mappedPath.indexOf('.') != -1); + if (!hasSuffix && pathMatcher.match(mappedPath + ".*", lookupPath)) { + return true; + } + return (!mappedPath.startsWith("/") && + (lookupPath.endsWith(mappedPath) || pathMatcher.match("/**/" + mappedPath, lookupPath) || + (!hasSuffix && pathMatcher.match("/**/" + mappedPath + ".*", lookupPath)))); + } + + private boolean checkParameters(RequestMappingInfo mapping, HttpServletRequest request) { + return ServletAnnotationMappingUtils.checkRequestMethod(mapping.methods, request) && + ServletAnnotationMappingUtils.checkParameters(mapping.params, request); + } + + private boolean isBetterPathMatch(String mappedPath, String mappedPathToCompare, String lookupPath) { + return (mappedPath != null && + (mappedPathToCompare == null || mappedPathToCompare.length() < mappedPath.length() || + (mappedPath.equals(lookupPath) && !mappedPathToCompare.equals(lookupPath)))); + } + + private boolean isBetterMethodMatch(RequestMappingInfo mapping, RequestMappingInfo mappingToCompare) { + return (mappingToCompare.methods.length == 0 && mapping.methods.length > 0); + } + + private boolean isBetterParamMatch(RequestMappingInfo mapping, RequestMappingInfo mappingToCompare) { + return (mappingToCompare.params.length < mapping.params.length); + } + } + + + private class ServletHandlerMethodInvoker extends HandlerMethodInvoker { + + private boolean responseArgumentUsed = false; + + public ServletHandlerMethodInvoker(HandlerMethodResolver resolver) { + super(resolver, webBindingInitializer, sessionAttributeStore, + parameterNameDiscoverer, customArgumentResolvers); + } + + @Override + protected void raiseMissingParameterException(String paramName, Class paramType) throws Exception { + throw new MissingServletRequestParameterException(paramName, paramType.getName()); + } + + @Override + protected void raiseSessionRequiredException(String message) throws Exception { + throw new HttpSessionRequiredException(message); + } + + @Override + protected WebDataBinder createBinder(NativeWebRequest webRequest, Object target, String objectName) + throws Exception { + + return AnnotationMethodHandlerAdapter.this.createBinder( + (HttpServletRequest) webRequest.getNativeRequest(), target, objectName); + } + + @Override + protected void doBind(NativeWebRequest webRequest, WebDataBinder binder, boolean failOnErrors) + throws Exception { + + ServletRequestDataBinder servletBinder = (ServletRequestDataBinder) binder; + servletBinder.bind((ServletRequest) webRequest.getNativeRequest()); + if (failOnErrors) { + servletBinder.closeNoCatch(); + } + } + + @Override + protected Object resolveStandardArgument(Class parameterType, NativeWebRequest webRequest) + throws Exception { + + HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); + HttpServletResponse response = (HttpServletResponse) webRequest.getNativeResponse(); + + if (ServletRequest.class.isAssignableFrom(parameterType)) { + return request; + } + else if (ServletResponse.class.isAssignableFrom(parameterType)) { + this.responseArgumentUsed = true; + return response; + } + else if (HttpSession.class.isAssignableFrom(parameterType)) { + return request.getSession(); + } + else if (Principal.class.isAssignableFrom(parameterType)) { + return request.getUserPrincipal(); + } + else if (Locale.class.equals(parameterType)) { + return RequestContextUtils.getLocale(request); + } + else if (InputStream.class.isAssignableFrom(parameterType)) { + return request.getInputStream(); + } + else if (Reader.class.isAssignableFrom(parameterType)) { + return request.getReader(); + } + else if (OutputStream.class.isAssignableFrom(parameterType)) { + this.responseArgumentUsed = true; + return response.getOutputStream(); + } + else if (Writer.class.isAssignableFrom(parameterType)) { + this.responseArgumentUsed = true; + return response.getWriter(); + } + return super.resolveStandardArgument(parameterType, webRequest); + } + + @SuppressWarnings("unchecked") + public ModelAndView getModelAndView(Method handlerMethod, Class handlerType, Object returnValue, + ExtendedModelMap implicitModel, ServletWebRequest webRequest) { + + if (returnValue instanceof ModelAndView) { + ModelAndView mav = (ModelAndView) returnValue; + mav.getModelMap().mergeAttributes(implicitModel); + return mav; + } + else if (returnValue instanceof Model) { + return new ModelAndView().addAllObjects(implicitModel).addAllObjects(((Model) returnValue).asMap()); + } + else if (returnValue instanceof Map) { + return new ModelAndView().addAllObjects(implicitModel).addAllObjects((Map) returnValue); + } + else if (returnValue instanceof View) { + return new ModelAndView((View) returnValue).addAllObjects(implicitModel); + } + else if (returnValue instanceof String) { + return new ModelAndView((String) returnValue).addAllObjects(implicitModel); + } + else if (returnValue == null) { + // Either returned null or was 'void' return. + if (this.responseArgumentUsed || webRequest.isNotModified()) { + return null; + } + else { + // Assuming view name translation... + return new ModelAndView().addAllObjects(implicitModel); + } + } + else if (!BeanUtils.isSimpleProperty(returnValue.getClass())) { + // Assume a single model attribute... + ModelAttribute attr = AnnotationUtils.findAnnotation(handlerMethod, ModelAttribute.class); + String attrName = (attr != null ? attr.value() : ""); + ModelAndView mav = new ModelAndView().addAllObjects(implicitModel); + if ("".equals(attrName)) { + Class resolvedType = GenericTypeResolver.resolveReturnType(handlerMethod, handlerType); + attrName = Conventions.getVariableNameForReturnType(handlerMethod, resolvedType, returnValue); + } + return mav.addObject(attrName, returnValue); + } + else { + throw new IllegalArgumentException("Invalid handler method return value: " + returnValue); + } + } + } + + + private static class RequestMappingInfo { + + public String[] paths = new String[0]; + + public RequestMethod[] methods = new RequestMethod[0]; + + public String[] params = new String[0]; + + public boolean equals(Object obj) { + RequestMappingInfo other = (RequestMappingInfo) obj; + return (Arrays.equals(this.paths, other.paths) && Arrays.equals(this.methods, other.methods) && + Arrays.equals(this.params, other.params)); + } + + public int hashCode() { + return (Arrays.hashCode(this.paths) * 29 + Arrays.hashCode(this.methods) * 31 + + Arrays.hashCode(this.params)); + } + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/DefaultAnnotationHandlerMapping.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/DefaultAnnotationHandlerMapping.java new file mode 100644 index 00000000000..a4d400d9636 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/DefaultAnnotationHandlerMapping.java @@ -0,0 +1,215 @@ +/* + * Copyright 2002-2008 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.web.servlet.mvc.annotation; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; + +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.generic.GenericBeanFactoryAccessor; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.stereotype.Controller; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.servlet.handler.AbstractDetectingUrlHandlerMapping; + +/** + * Implementation of the {@link org.springframework.web.servlet.HandlerMapping} + * interface that maps handlers based on HTTP paths expressed through the + * {@link RequestMapping} annotation at the type or method level. + * + *

Registered by default in {@link org.springframework.web.servlet.DispatcherServlet} + * on Java 5+. NOTE: If you define custom HandlerMapping beans in your + * DispatcherServlet context, you need to add a DefaultAnnotationHandlerMapping bean + * explicitly, since custom HandlerMapping beans replace the default mapping strategies. + * Defining a DefaultAnnotationHandlerMapping also allows for registering custom + * interceptors: + * + *

+ * <bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping">
+ *   <property name="interceptors">
+ *     ...
+ *   </property>
+ * </bean>
+ * + * Annotated controllers are usually marked with the {@link Controller} stereotype + * at the type level. This is not strictly necessary when {@link RequestMapping} is + * applied at the type level (since such a handler usually implements the + * {@link org.springframework.web.servlet.mvc.Controller} interface). However, + * {@link Controller} is required for detecting {@link RequestMapping} annotations + * at the method level if {@link RequestMapping} is not present at the type level. + * + *

NOTE: Method-level mappings are only allowed to narrow the mapping + * expressed at the class level (if any). HTTP paths need to uniquely map onto + * specific handler beans, with any given HTTP path only allowed to be mapped + * onto one specific handler bean (not spread across multiple handler beans). + * It is strongly recommended to co-locate related handler methods into the same bean. + * + *

The {@link AnnotationMethodHandlerAdapter} is responsible for processing + * annotated handler methods, as mapped by this HandlerMapping. For + * {@link RequestMapping} at the type level, specific HandlerAdapters such as + * {@link org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter} apply. + * + * @author Juergen Hoeller + * @author Arjen Poutsma + * @since 2.5 + * @see RequestMapping + * @see AnnotationMethodHandlerAdapter + */ +public class DefaultAnnotationHandlerMapping extends AbstractDetectingUrlHandlerMapping { + + private boolean useDefaultSuffixPattern = true; + + private final Map cachedMappings = new HashMap(); + + + /** + * Set whether to register paths using the default suffix pattern as well: + * i.e. whether "/users" should be registered as "/users.*" too. + *

Default is "true". Turn this convention off if you intend to interpret + * your @RequestMapping paths strictly. + *

Note that paths which include a ".xxx" suffix already will not be + * transformed using the default suffix pattern in any case. + */ + public void setUseDefaultSuffixPattern(boolean useDefaultSuffixPattern) { + this.useDefaultSuffixPattern = useDefaultSuffixPattern; + } + + + /** + * Checks for presence of the {@link org.springframework.web.bind.annotation.RequestMapping} + * annotation on the handler class and on any of its methods. + */ + protected String[] determineUrlsForHandler(String beanName) { + ApplicationContext context = getApplicationContext(); + Class handlerType = context.getType(beanName); + ListableBeanFactory bf = (context instanceof ConfigurableApplicationContext ? + ((ConfigurableApplicationContext) context).getBeanFactory() : context); + GenericBeanFactoryAccessor bfa = new GenericBeanFactoryAccessor(bf); + RequestMapping mapping = bfa.findAnnotationOnBean(beanName, RequestMapping.class); + + if (mapping != null) { + // @RequestMapping found at type level + this.cachedMappings.put(handlerType, mapping); + Set urls = new LinkedHashSet(); + String[] paths = mapping.value(); + if (paths.length > 0) { + // @RequestMapping specifies paths at type level + for (String path : paths) { + addUrlsForPath(urls, path); + } + return StringUtils.toStringArray(urls); + } + else { + // actual paths specified by @RequestMapping at method level + return determineUrlsForHandlerMethods(handlerType); + } + } + else if (AnnotationUtils.findAnnotation(handlerType, Controller.class) != null) { + // @RequestMapping to be introspected at method level + return determineUrlsForHandlerMethods(handlerType); + } + else { + return null; + } + } + + /** + * Derive URL mappings from the handler's method-level mappings. + * @param handlerType the handler type to introspect + * @return the array of mapped URLs + */ + protected String[] determineUrlsForHandlerMethods(Class handlerType) { + final Set urls = new LinkedHashSet(); + ReflectionUtils.doWithMethods(handlerType, new ReflectionUtils.MethodCallback() { + public void doWith(Method method) { + RequestMapping mapping = method.getAnnotation(RequestMapping.class); + if (mapping != null) { + String[] mappedPaths = mapping.value(); + for (int i = 0; i < mappedPaths.length; i++) { + addUrlsForPath(urls, mappedPaths[i]); + } + } + } + }); + return StringUtils.toStringArray(urls); + } + + /** + * Add URLs and/or URL patterns for the given path. + * @param urls the Set of URLs for the current bean + * @param path the currently introspected path + */ + protected void addUrlsForPath(Set urls, String path) { + urls.add(path); + if (this.useDefaultSuffixPattern && path.indexOf('.') == -1) { + urls.add(path + ".*"); + } + } + + + /** + * Validate the given annotated handler against the current request. + * @see #validateMapping + */ + protected void validateHandler(Object handler, HttpServletRequest request) throws Exception { + RequestMapping mapping = this.cachedMappings.get(handler.getClass()); + if (mapping == null) { + mapping = AnnotationUtils.findAnnotation(handler.getClass(), RequestMapping.class); + } + if (mapping != null) { + validateMapping(mapping, request); + } + } + + /** + * Validate the given type-level mapping metadata against the current request, + * checking HTTP request method and parameter conditions. + * @param mapping the mapping metadata to validate + * @param request current HTTP request + * @throws Exception if validation failed + */ + protected void validateMapping(RequestMapping mapping, HttpServletRequest request) throws Exception { + RequestMethod[] mappedMethods = mapping.method(); + if (!ServletAnnotationMappingUtils.checkRequestMethod(mappedMethods, request)) { + String[] supportedMethods = new String[mappedMethods.length]; + for (int i = 0; i < mappedMethods.length; i++) { + supportedMethods[i] = mappedMethods[i].name(); + } + throw new HttpRequestMethodNotSupportedException(request.getMethod(), supportedMethods); + } + + String[] mappedParams = mapping.params(); + if (!ServletAnnotationMappingUtils.checkParameters(mappedParams, request)) { + throw new ServletException("Parameter conditions {" + + StringUtils.arrayToDelimitedString(mappedParams, ", ") + + "} not met for request parameters: " + request.getParameterMap()); + } + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationMappingUtils.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationMappingUtils.java new file mode 100644 index 00000000000..3b757036453 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationMappingUtils.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2008 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.web.servlet.mvc.annotation; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.util.WebUtils; + +/** + * Helper class for annotation-based request mapping. + * + * @author Juergen Hoeller + * @since 2.5.2 + */ +abstract class ServletAnnotationMappingUtils { + + /** + * Check whether the given request matches the specified request methods. + * @param methods the HTTP request methods to check against + * @param request the current HTTP request to check + */ + public static boolean checkRequestMethod(RequestMethod[] methods, HttpServletRequest request) { + if (!ObjectUtils.isEmpty(methods)) { + boolean match = false; + for (RequestMethod method : methods) { + if (method.name().equals(request.getMethod())) { + match = true; + } + } + if (!match) { + return false; + } + } + return true; + } + + /** + * Check whether the given request matches the specified parameter conditions. + * @param params the parameter conditions, following + * {@link org.springframework.web.bind.annotation.RequestMapping#params()} + * @param request the current HTTP request to check + */ + public static boolean checkParameters(String[] params, HttpServletRequest request) { + if (!ObjectUtils.isEmpty(params)) { + for (String param : params) { + int separator = param.indexOf('='); + if (separator == -1) { + if (param.startsWith("!")) { + if (WebUtils.hasSubmitParameter(request, param.substring(1))) { + return false; + } + } + else if (!WebUtils.hasSubmitParameter(request, param)) { + return false; + } + } + else { + String key = param.substring(0, separator); + String value = param.substring(separator + 1); + if (!value.equals(request.getParameter(key))) { + return false; + } + } + } + } + return true; + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/package.html b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/package.html new file mode 100644 index 00000000000..66575d8f655 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/package.html @@ -0,0 +1,7 @@ + + + +Support package for annotation-based Servlet MVC controllers. + + + diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/multiaction/AbstractUrlMethodNameResolver.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/multiaction/AbstractUrlMethodNameResolver.java new file mode 100644 index 00000000000..a43cece2df1 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/multiaction/AbstractUrlMethodNameResolver.java @@ -0,0 +1,115 @@ +/* + * Copyright 2002-2008 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.web.servlet.mvc.multiaction; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.util.Assert; +import org.springframework.web.util.UrlPathHelper; + +/** + * Abstract base class for URL-based {@link MethodNameResolver} implementations. + * + *

Provides infrastructure for mapping handlers to URLs and configurable + * URL lookup. For information on the latter, see the + * {@link #setAlwaysUseFullPath} "alwaysUseFullPath"} + * and {@link #setUrlDecode "urlDecode"} properties. + * + * @author Juergen Hoeller + * @since 14.01.2004 + */ +public abstract class AbstractUrlMethodNameResolver implements MethodNameResolver { + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + private UrlPathHelper urlPathHelper = new UrlPathHelper(); + + + /** + * Set if URL lookup should always use full path within current servlet + * context. Else, the path within the current servlet mapping is used + * if applicable (i.e. in the case of a ".../*" servlet mapping in web.xml). + * Default is "false". + * @see org.springframework.web.util.UrlPathHelper#setAlwaysUseFullPath + */ + public void setAlwaysUseFullPath(boolean alwaysUseFullPath) { + this.urlPathHelper.setAlwaysUseFullPath(alwaysUseFullPath); + } + + /** + * Set if context path and request URI should be URL-decoded. + * Both are returned undecoded by the Servlet API, + * in contrast to the servlet path. + *

Uses either the request encoding or the default encoding according + * to the Servlet spec (ISO-8859-1). + * @see org.springframework.web.util.UrlPathHelper#setUrlDecode + */ + public void setUrlDecode(boolean urlDecode) { + this.urlPathHelper.setUrlDecode(urlDecode); + } + + /** + * Set the UrlPathHelper to use for resolution of lookup paths. + *

Use this to override the default UrlPathHelper with a custom subclass, + * or to share common UrlPathHelper settings across multiple MethodNameResolvers + * and HandlerMappings. + * @see org.springframework.web.servlet.handler.AbstractUrlHandlerMapping#setUrlPathHelper + */ + public void setUrlPathHelper(UrlPathHelper urlPathHelper) { + Assert.notNull(urlPathHelper, "UrlPathHelper must not be null"); + this.urlPathHelper = urlPathHelper; + } + + + /** + * Retrieves the URL path to use for lookup and delegates to + * getHandlerMethodNameForUrlPath. + * Converts null values to NoSuchRequestHandlingMethodExceptions. + * @see #getHandlerMethodNameForUrlPath + */ + public final String getHandlerMethodName(HttpServletRequest request) + throws NoSuchRequestHandlingMethodException { + + String urlPath = this.urlPathHelper.getLookupPathForRequest(request); + String name = getHandlerMethodNameForUrlPath(urlPath); + if (name == null) { + throw new NoSuchRequestHandlingMethodException(urlPath, request.getMethod(), request.getParameterMap()); + } + if (logger.isDebugEnabled()) { + logger.debug("Returning handler method name '" + name + "' for lookup path: " + urlPath); + } + return name; + } + + /** + * Return a method name that can handle this request, based on the + * given lookup path. Called by getHandlerMethodName. + * @param urlPath the URL path to use for lookup, + * according to the settings in this class + * @return a method name that can handle this request. + * Should return null if no matching method found. + * @see #getHandlerMethodName + * @see #setAlwaysUseFullPath + * @see #setUrlDecode + */ + protected abstract String getHandlerMethodNameForUrlPath(String urlPath); + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/multiaction/InternalPathMethodNameResolver.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/multiaction/InternalPathMethodNameResolver.java new file mode 100644 index 00000000000..89ee80de074 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/multiaction/InternalPathMethodNameResolver.java @@ -0,0 +1,123 @@ +/* + * Copyright 2002-2007 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.web.servlet.mvc.multiaction; + +import java.util.Map; + +import org.springframework.core.CollectionFactory; +import org.springframework.web.util.WebUtils; + +/** + * Simple implementation of {@link MethodNameResolver} that maps URL to + * method name. Although this is the default implementation used by the + * {@link MultiActionController} class (because it requires no configuration), + * it's bit naive for most applications. In particular, we don't usually + * want to tie URL to implementation methods. + * + *

Maps the resource name after the last slash, ignoring an extension. + * E.g. "/foo/bar/baz.html" to "baz", assuming a "/foo/bar/baz.html" + * controller mapping to the corresponding MultiActionController handler. + * method. Doesn't support wildcards. + * + * @author Rod Johnson + * @author Juergen Hoeller +*/ +public class InternalPathMethodNameResolver extends AbstractUrlMethodNameResolver { + + private String prefix = ""; + + private String suffix = ""; + + /** Request URL path String --> method name String */ + private final Map methodNameCache = CollectionFactory.createConcurrentMapIfPossible(16); + + + /** + * Specify a common prefix for handler method names. + * Will be prepended to the internal path found in the URL: + * e.g. internal path "baz", prefix "my" -> method name "mybaz". + */ + public void setPrefix(String prefix) { + this.prefix = (prefix != null ? prefix : ""); + } + + /** + * Return the common prefix for handler method names. + */ + protected String getPrefix() { + return this.prefix; + } + + /** + * Specify a common suffix for handler method names. + * Will be appended to the internal path found in the URL: + * e.g. internal path "baz", suffix "Handler" -> method name "bazHandler". + */ + public void setSuffix(String suffix) { + this.suffix = (suffix != null ? suffix : ""); + } + + /** + * Return the common suffix for handler method names. + */ + protected String getSuffix() { + return this.suffix; + } + + + /** + * Extracts the method name indicated by the URL path. + * @see #extractHandlerMethodNameFromUrlPath + * @see #postProcessHandlerMethodName + */ + protected String getHandlerMethodNameForUrlPath(String urlPath) { + String methodName = (String) this.methodNameCache.get(urlPath); + if (methodName == null) { + methodName = extractHandlerMethodNameFromUrlPath(urlPath); + methodName = postProcessHandlerMethodName(methodName); + this.methodNameCache.put(urlPath, methodName); + } + return methodName; + } + + /** + * Extract the handler method name from the given request URI. + * Delegates to WebUtils.extractViewNameFromUrlPath(String). + * @param uri the request URI (e.g. "/index.html") + * @return the extracted URI filename (e.g. "index") + * @see org.springframework.web.util.WebUtils#extractFilenameFromUrlPath + */ + protected String extractHandlerMethodNameFromUrlPath(String uri) { + return WebUtils.extractFilenameFromUrlPath(uri); + } + + /** + * Build the full handler method name based on the given method name + * as indicated by the URL path. + *

The default implementation simply applies prefix and suffix. + * This can be overridden, for example, to manipulate upper case + * / lower case, etc. + * @param methodName the original method name, as indicated by the URL path + * @return the full method name to use + * @see #getPrefix() + * @see #getSuffix() + */ + protected String postProcessHandlerMethodName(String methodName) { + return getPrefix() + methodName + getSuffix(); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/multiaction/MethodNameResolver.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/multiaction/MethodNameResolver.java new file mode 100644 index 00000000000..d073b320731 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/multiaction/MethodNameResolver.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2006 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.web.servlet.mvc.multiaction; + +import javax.servlet.http.HttpServletRequest; + +/** + * Interface that parameterizes the MultiActionController class + * using the Strategy GoF Design pattern, allowing + * the mapping from incoming request to handler method name + * to be varied without affecting other application code. + * + *

Illustrates how delegation can be more flexible than subclassing. + * + * @author Rod Johnson + * @see MultiActionController#setMethodNameResolver + */ +public interface MethodNameResolver { + + /** + * Return a method name that can handle this request. Such + * mappings are typically, but not necessarily, based on URL. + * @param request current HTTP request + * @return a method name that can handle this request. + * Never returns null; throws exception if not resolvable. + * @throws NoSuchRequestHandlingMethodException if no handler method + * can be found for the given request + */ + String getHandlerMethodName(HttpServletRequest request) throws NoSuchRequestHandlingMethodException; + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/multiaction/MultiActionController.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/multiaction/MultiActionController.java new file mode 100644 index 00000000000..ac3e42e7042 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/multiaction/MultiActionController.java @@ -0,0 +1,665 @@ +/* + * Copyright 2002-2008 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.web.servlet.mvc.multiaction; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.BeanUtils; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; +import org.springframework.validation.ValidationUtils; +import org.springframework.validation.Validator; +import org.springframework.web.HttpSessionRequiredException; +import org.springframework.web.bind.ServletRequestDataBinder; +import org.springframework.web.bind.support.WebBindingInitializer; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.AbstractController; +import org.springframework.web.servlet.mvc.LastModified; + +/** + * {@link org.springframework.web.servlet.mvc.Controller Controller} + * implementation that allows multiple request types to be handled by the same + * class. Subclasses of this class can handle several different types of + * request with methods of the form + * + *

public (ModelAndView | Map | String | void) actionName(HttpServletRequest request, HttpServletResponse response);
+ * + * A Map return value indicates a model that is supposed to be passed to a default view + * (determined through a {@link org.springframework.web.servlet.RequestToViewNameTranslator}). + * A String return value indicates the name of a view to be rendered without a specific model. + * + *

May take a third parameter (of type {@link HttpSession}) in which an + * existing session will be required, or a third parameter of an arbitrary + * class that gets treated as the command (that is, an instance of the class + * gets created, and request parameters get bound to it) + * + *

These methods can throw any kind of exception, but should only let + * propagate those that they consider fatal, or which their class or superclass + * is prepared to catch by implementing an exception handler. + * + *

When returning just a {@link Map} instance view name translation will be + * used to generate the view name. The configured + * {@link org.springframework.web.servlet.RequestToViewNameTranslator} will be + * used to determine the view name. + * + *

When returning void a return value of null is + * assumed meaning that the handler method is responsible for writing the + * response directly to the supplied {@link HttpServletResponse}. + * + *

This model allows for rapid coding, but loses the advantage of + * compile-time checking. It is similar to a Struts DispatchAction, + * but more sophisticated. Also supports delegation to another object. + * + *

An implementation of the {@link MethodNameResolver} interface defined in + * this package should return a method name for a given request, based on any + * aspect of the request, such as its URL or an "action" parameter. The actual + * strategy can be configured via the "methodNameResolver" bean property, for + * each MultiActionController. + * + *

The default MethodNameResolver is + * {@link InternalPathMethodNameResolver}; further included strategies are + * {@link PropertiesMethodNameResolver} and {@link ParameterMethodNameResolver}. + * + *

Subclasses can implement custom exception handler methods with names such + * as: + * + *

public ModelAndView anyMeaningfulName(HttpServletRequest request, HttpServletResponse response, ExceptionClass exception);
+ * + * The third parameter can be any subclass or {@link Exception} or + * {@link RuntimeException}. + * + *

There can also be an optional xxxLastModified method for + * handlers, of signature: + * + *

public long anyMeaningfulNameLastModified(HttpServletRequest request)
+ * + * If such a method is present, it will be invoked. Default return from + * getLastModified is -1, meaning that the content must always be + * regenerated. + * + *

Note that all handler methods need to be public and that + * method overloading is not allowed. + * + *

See also the description of the workflow performed by + * {@link AbstractController the superclass} (in that section of the class + * level Javadoc entitled 'workflow'). + * + *

Note: For maximum data binding flexibility, consider direct usage of a + * {@link ServletRequestDataBinder} in your controller method, instead of relying + * on a declared command argument. This allows for full control over the entire + * binder setup and usage, including the invocation of {@link Validator Validators} + * and the subsequent evaluation of binding/validation errors. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Colin Sampaleanu + * @author Rob Harrop + * @author Sam Brannen + * @see MethodNameResolver + * @see InternalPathMethodNameResolver + * @see PropertiesMethodNameResolver + * @see ParameterMethodNameResolver + * @see org.springframework.web.servlet.mvc.LastModified#getLastModified + * @see org.springframework.web.bind.ServletRequestDataBinder + */ +public class MultiActionController extends AbstractController implements LastModified { + + /** Suffix for last-modified methods */ + public static final String LAST_MODIFIED_METHOD_SUFFIX = "LastModified"; + + /** Default command name used for binding command objects: "command" */ + public static final String DEFAULT_COMMAND_NAME = "command"; + + /** + * Log category to use when no mapped handler is found for a request. + * @see #pageNotFoundLogger + */ + public static final String PAGE_NOT_FOUND_LOG_CATEGORY = "org.springframework.web.servlet.PageNotFound"; + + + /** + * Additional logger to use when no mapped handler is found for a request. + * @see #PAGE_NOT_FOUND_LOG_CATEGORY + */ + protected static final Log pageNotFoundLogger = LogFactory.getLog(PAGE_NOT_FOUND_LOG_CATEGORY); + + /** Object we'll invoke methods on. Defaults to this. */ + private Object delegate; + + /** Delegate that knows how to determine method names from incoming requests */ + private MethodNameResolver methodNameResolver = new InternalPathMethodNameResolver(); + + /** List of Validators to apply to commands */ + private Validator[] validators; + + /** Optional strategy for pre-initializing data binding */ + private WebBindingInitializer webBindingInitializer; + + /** Handler methods, keyed by name */ + private final Map handlerMethodMap = new HashMap(); + + /** LastModified methods, keyed by handler method name (without LAST_MODIFIED_SUFFIX) */ + private final Map lastModifiedMethodMap = new HashMap(); + + /** Methods, keyed by exception class */ + private final Map exceptionHandlerMap = new HashMap(); + + + /** + * Constructor for MultiActionController that looks for + * handler methods in the present subclass. + */ + public MultiActionController() { + this.delegate = this; + registerHandlerMethods(this.delegate); + // We'll accept no handler methods found here - a delegate might be set later on. + } + + /** + * Constructor for MultiActionController that looks for + * handler methods in delegate, rather than a subclass of this class. + * @param delegate handler object. This does not need to implement any + * particular interface, as everything is done using reflection. + */ + public MultiActionController(Object delegate) { + setDelegate(delegate); + } + + + /** + * Set the delegate used by this class; the default is this, + * assuming that handler methods have been added by a subclass. + *

This method does not get invoked once the class is configured. + * @param delegate an object containing handler methods + * @throws IllegalStateException if no handler methods are found + */ + public final void setDelegate(Object delegate) { + Assert.notNull(delegate, "Delegate must not be null"); + this.delegate = delegate; + registerHandlerMethods(this.delegate); + // There must be SOME handler methods. + if (this.handlerMethodMap.isEmpty()) { + throw new IllegalStateException("No handler methods in class [" + this.delegate.getClass() + "]"); + } + } + + /** + * Set the method name resolver that this class should use. + *

Allows parameterization of handler method mappings. + */ + public final void setMethodNameResolver(MethodNameResolver methodNameResolver) { + this.methodNameResolver = methodNameResolver; + } + + /** + * Return the MethodNameResolver used by this class. + */ + public final MethodNameResolver getMethodNameResolver() { + return this.methodNameResolver; + } + + /** + * Set the {@link Validator Validators} for this controller. + *

The Validators must support the specified command class. + */ + public final void setValidators(Validator[] validators) { + this.validators = validators; + } + + /** + * Return the Validators for this controller. + */ + public final Validator[] getValidators() { + return this.validators; + } + + /** + * Specify a WebBindingInitializer which will apply pre-configured + * configuration to every DataBinder that this controller uses. + *

Allows for factoring out the entire binder configuration + * to separate objects, as an alternative to {@link #initBinder}. + */ + public final void setWebBindingInitializer(WebBindingInitializer webBindingInitializer) { + this.webBindingInitializer = webBindingInitializer; + } + + /** + * Return the WebBindingInitializer (if any) which will apply pre-configured + * configuration to every DataBinder that this controller uses. + */ + public final WebBindingInitializer getWebBindingInitializer() { + return this.webBindingInitializer; + } + + + /** + * Registers all handlers methods on the delegate object. + */ + private void registerHandlerMethods(Object delegate) { + this.handlerMethodMap.clear(); + this.lastModifiedMethodMap.clear(); + this.exceptionHandlerMap.clear(); + + // Look at all methods in the subclass, trying to find + // methods that are validators according to our criteria + Method[] methods = delegate.getClass().getMethods(); + for (int i = 0; i < methods.length; i++) { + // We're looking for methods with given parameters. + Method method = methods[i]; + if (isExceptionHandlerMethod(method)) { + registerExceptionHandlerMethod(method); + } + else if (isHandlerMethod(method)) { + registerHandlerMethod(method); + registerLastModifiedMethodIfExists(delegate, method); + } + } + } + + /** + * Is the supplied method a valid handler method? + *

Does not consider Controller.handleRequest itself + * as handler method (to avoid potential stack overflow). + */ + private boolean isHandlerMethod(Method method) { + Class returnType = method.getReturnType(); + if (ModelAndView.class.equals(returnType) || Map.class.equals(returnType) || String.class.equals(returnType) || + void.class.equals(returnType)) { + Class[] parameterTypes = method.getParameterTypes(); + return (parameterTypes.length >= 2 && + HttpServletRequest.class.equals(parameterTypes[0]) && + HttpServletResponse.class.equals(parameterTypes[1]) && + !("handleRequest".equals(method.getName()) && parameterTypes.length == 2)); + } + return false; + } + + /** + * Is the supplied method a valid exception handler method? + */ + private boolean isExceptionHandlerMethod(Method method) { + return (isHandlerMethod(method) && + method.getParameterTypes().length == 3 && + Throwable.class.isAssignableFrom(method.getParameterTypes()[2])); + } + + /** + * Registers the supplied method as a request handler. + */ + private void registerHandlerMethod(Method method) { + if (logger.isDebugEnabled()) { + logger.debug("Found action method [" + method + "]"); + } + this.handlerMethodMap.put(method.getName(), method); + } + + /** + * Registers a last-modified handler method for the supplied handler method + * if one exists. + */ + private void registerLastModifiedMethodIfExists(Object delegate, Method method) { + // Look for corresponding LastModified method. + try { + Method lastModifiedMethod = delegate.getClass().getMethod( + method.getName() + LAST_MODIFIED_METHOD_SUFFIX, + new Class[] {HttpServletRequest.class}); + Class returnType = lastModifiedMethod.getReturnType(); + if (!(long.class.equals(returnType) || Long.class.equals(returnType))) { + throw new IllegalStateException("last-modified method [" + lastModifiedMethod + + "] declares an invalid return type - needs to be 'long' or 'Long'"); + } + // Put in cache, keyed by handler method name. + this.lastModifiedMethodMap.put(method.getName(), lastModifiedMethod); + if (logger.isDebugEnabled()) { + logger.debug("Found last-modified method for handler method [" + method + "]"); + } + } + catch (NoSuchMethodException ex) { + // No last modified method. That's ok. + } + } + + /** + * Registers the supplied method as an exception handler. + */ + private void registerExceptionHandlerMethod(Method method) { + this.exceptionHandlerMap.put(method.getParameterTypes()[2], method); + if (logger.isDebugEnabled()) { + logger.debug("Found exception handler method [" + method + "]"); + } + } + + + //--------------------------------------------------------------------- + // Implementation of LastModified + //--------------------------------------------------------------------- + + /** + * Try to find an XXXXLastModified method, where XXXX is the name of a handler. + * Return -1 if there's no such handler, indicating that content must be updated. + * @see org.springframework.web.servlet.mvc.LastModified#getLastModified(HttpServletRequest) + */ + public long getLastModified(HttpServletRequest request) { + try { + String handlerMethodName = this.methodNameResolver.getHandlerMethodName(request); + Method lastModifiedMethod = (Method) this.lastModifiedMethodMap.get(handlerMethodName); + if (lastModifiedMethod != null) { + try { + // Invoke the last-modified method... + Long wrappedLong = (Long) lastModifiedMethod.invoke(this.delegate, new Object[] {request}); + return (wrappedLong != null ? wrappedLong.longValue() : -1); + } + catch (Exception ex) { + // We encountered an error invoking the last-modified method. + // We can't do anything useful except log this, as we can't throw an exception. + logger.error("Failed to invoke last-modified method", ex); + } + } + } + catch (NoSuchRequestHandlingMethodException ex) { + // No handler method for this request. This shouldn't happen, as this + // method shouldn't be called unless a previous invocation of this class + // has generated content. Do nothing, that's OK: We'll return default. + } + return -1L; + } + + + //--------------------------------------------------------------------- + // Implementation of AbstractController + //--------------------------------------------------------------------- + + /** + * Determine a handler method and invoke it. + * @see MethodNameResolver#getHandlerMethodName + * @see #invokeNamedMethod + * @see #handleNoSuchRequestHandlingMethod + */ + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) + throws Exception { + try { + String methodName = this.methodNameResolver.getHandlerMethodName(request); + return invokeNamedMethod(methodName, request, response); + } + catch (NoSuchRequestHandlingMethodException ex) { + return handleNoSuchRequestHandlingMethod(ex, request, response); + } + } + + /** + * Handle the case where no request handler method was found. + *

The default implementation logs a warning and sends an HTTP 404 error. + * Alternatively, a fallback view could be chosen, or the + * NoSuchRequestHandlingMethodException could be rethrown as-is. + * @param ex the NoSuchRequestHandlingMethodException to be handled + * @param request current HTTP request + * @param response current HTTP response + * @return a ModelAndView to render, or null if handled directly + * @throws Exception an Exception that should be thrown as result of the servlet request + */ + protected ModelAndView handleNoSuchRequestHandlingMethod( + NoSuchRequestHandlingMethodException ex, HttpServletRequest request, HttpServletResponse response) + throws Exception { + + pageNotFoundLogger.warn(ex.getMessage()); + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return null; + } + + /** + * Invokes the named method. + *

Uses a custom exception handler if possible; otherwise, throw an + * unchecked exception; wrap a checked exception or Throwable. + */ + protected final ModelAndView invokeNamedMethod( + String methodName, HttpServletRequest request, HttpServletResponse response) throws Exception { + + Method method = (Method) this.handlerMethodMap.get(methodName); + if (method == null) { + throw new NoSuchRequestHandlingMethodException(methodName, getClass()); + } + + try { + Class[] paramTypes = method.getParameterTypes(); + List params = new ArrayList(4); + params.add(request); + params.add(response); + + if (paramTypes.length >= 3 && paramTypes[2].equals(HttpSession.class)) { + HttpSession session = request.getSession(false); + if (session == null) { + throw new HttpSessionRequiredException( + "Pre-existing session required for handler method '" + methodName + "'"); + } + params.add(session); + } + + // If last parameter isn't of HttpSession type, it's a command. + if (paramTypes.length >= 3 && + !paramTypes[paramTypes.length - 1].equals(HttpSession.class)) { + Object command = newCommandObject(paramTypes[paramTypes.length - 1]); + params.add(command); + bind(request, command); + } + + Object returnValue = method.invoke(this.delegate, params.toArray(new Object[params.size()])); + return massageReturnValueIfNecessary(returnValue); + } + catch (InvocationTargetException ex) { + // The handler method threw an exception. + return handleException(request, response, ex.getTargetException()); + } + catch (Exception ex) { + // The binding process threw an exception. + return handleException(request, response, ex); + } + } + + /** + * Processes the return value of a handler method to ensure that it either returns + * null or an instance of {@link ModelAndView}. When returning a {@link Map}, + * the {@link Map} instance is wrapped in a new {@link ModelAndView} instance. + */ + private ModelAndView massageReturnValueIfNecessary(Object returnValue) { + if (returnValue instanceof ModelAndView) { + return (ModelAndView) returnValue; + } + else if (returnValue instanceof Map) { + return new ModelAndView().addAllObjects((Map) returnValue); + } + else if (returnValue instanceof String) { + return new ModelAndView((String) returnValue); + } + else { + // Either returned null or was 'void' return. + // We'll assume that the handle method already wrote the response. + return null; + } + } + + + /** + * Create a new command object of the given class. + *

This implementation uses BeanUtils.instantiateClass, + * so commands need to have public no-arg constructors. + * Subclasses can override this implementation if desired. + * @throws Exception if the command object could not be instantiated + * @see org.springframework.beans.BeanUtils#instantiateClass(Class) + */ + protected Object newCommandObject(Class clazz) throws Exception { + if (logger.isDebugEnabled()) { + logger.debug("Creating new command of class [" + clazz.getName() + "]"); + } + return BeanUtils.instantiateClass(clazz); + } + + /** + * Bind request parameters onto the given command bean + * @param request request from which parameters will be bound + * @param command command object, that must be a JavaBean + * @throws Exception in case of invalid state or arguments + */ + protected void bind(HttpServletRequest request, Object command) throws Exception { + logger.debug("Binding request parameters onto MultiActionController command"); + ServletRequestDataBinder binder = createBinder(request, command); + binder.bind(request); + if (this.validators != null) { + for (int i = 0; i < this.validators.length; i++) { + if (this.validators[i].supports(command.getClass())) { + ValidationUtils.invokeValidator(this.validators[i], command, binder.getBindingResult()); + } + } + } + binder.closeNoCatch(); + } + + /** + * Create a new binder instance for the given command and request. + *

Called by bind. Can be overridden to plug in custom + * ServletRequestDataBinder subclasses. + *

The default implementation creates a standard ServletRequestDataBinder, + * and invokes initBinder. Note that initBinder + * will not be invoked if you override this method! + * @param request current HTTP request + * @param command the command to bind onto + * @return the new binder instance + * @throws Exception in case of invalid state or arguments + * @see #bind + * @see #initBinder + */ + protected ServletRequestDataBinder createBinder(HttpServletRequest request, Object command) throws Exception { + ServletRequestDataBinder binder = new ServletRequestDataBinder(command, getCommandName(command)); + initBinder(request, binder); + return binder; + } + + /** + * Return the command name to use for the given command object. + *

Default is "command". + * @param command the command object + * @return the command name to use + * @see #DEFAULT_COMMAND_NAME + */ + protected String getCommandName(Object command) { + return DEFAULT_COMMAND_NAME; + } + + /** + * Initialize the given binder instance, for example with custom editors. + * Called by createBinder. + *

This method allows you to register custom editors for certain fields of your + * command class. For instance, you will be able to transform Date objects into a + * String pattern and back, in order to allow your JavaBeans to have Date properties + * and still be able to set and display them in an HTML interface. + *

The default implementation is empty. + *

Note: the command object is not directly passed to this method, but it's available + * via {@link org.springframework.validation.DataBinder#getTarget()} + * @param request current HTTP request + * @param binder new binder instance + * @throws Exception in case of invalid state or arguments + * @see #createBinder + * @see org.springframework.validation.DataBinder#registerCustomEditor + * @see org.springframework.beans.propertyeditors.CustomDateEditor + */ + protected void initBinder(HttpServletRequest request, ServletRequestDataBinder binder) throws Exception { + if (this.webBindingInitializer != null) { + this.webBindingInitializer.initBinder(binder, new ServletWebRequest(request)); + } + initBinder((ServletRequest) request, binder); + } + + /** + * Initialize the given binder instance, for example with custom editors. + * @deprecated as of Spring 2.0: + * use initBinder(HttpServletRequest, ServletRequestDataBinder) instead + */ + protected void initBinder(ServletRequest request, ServletRequestDataBinder binder) throws Exception { + } + + + /** + * Determine the exception handler method for the given exception. + *

Can return null if not found. + * @return a handler for the given exception type, or null + * @param exception the exception to handle + */ + protected Method getExceptionHandler(Throwable exception) { + Class exceptionClass = exception.getClass(); + if (logger.isDebugEnabled()) { + logger.debug("Trying to find handler for exception class [" + exceptionClass.getName() + "]"); + } + Method handler = (Method) this.exceptionHandlerMap.get(exceptionClass); + while (handler == null && !exceptionClass.equals(Throwable.class)) { + if (logger.isDebugEnabled()) { + logger.debug("Trying to find handler for exception superclass [" + exceptionClass.getName() + "]"); + } + exceptionClass = exceptionClass.getSuperclass(); + handler = (Method) this.exceptionHandlerMap.get(exceptionClass); + } + return handler; + } + + /** + * We've encountered an exception thrown from a handler method. + * Invoke an appropriate exception handler method, if any. + * @param request current HTTP request + * @param response current HTTP response + * @param ex the exception that got thrown + * @return a ModelAndView to render the response + */ + private ModelAndView handleException(HttpServletRequest request, HttpServletResponse response, Throwable ex) + throws Exception { + + Method handler = getExceptionHandler(ex); + if (handler != null) { + if (logger.isDebugEnabled()) { + logger.debug("Invoking exception handler [" + handler + "] for exception: " + ex); + } + try { + Object returnValue = handler.invoke(this.delegate, new Object[] {request, response, ex}); + return massageReturnValueIfNecessary(returnValue); + } + catch (InvocationTargetException ex2) { + logger.error("Original exception overridden by exception handling failure", ex); + ReflectionUtils.rethrowException(ex2.getTargetException()); + } + catch (Exception ex2) { + logger.error("Failed to invoke exception handler method", ex2); + } + } + else { + // If we get here, there was no custom handler or we couldn't invoke it. + ReflectionUtils.rethrowException(ex); + } + throw new IllegalStateException("Should never get here"); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/multiaction/NoSuchRequestHandlingMethodException.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/multiaction/NoSuchRequestHandlingMethodException.java new file mode 100644 index 00000000000..2e110ed9df6 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/multiaction/NoSuchRequestHandlingMethodException.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2008 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.web.servlet.mvc.multiaction; + +import java.util.Map; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; + +import org.springframework.core.style.StylerUtils; +import org.springframework.web.util.UrlPathHelper; + +/** + * Exception thrown when there is no handler method ("action" method) + * for a specific HTTP request. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see MethodNameResolver#getHandlerMethodName(javax.servlet.http.HttpServletRequest) + */ +public class NoSuchRequestHandlingMethodException extends ServletException { + + private String methodName; + + + /** + * Create a new NoSuchRequestHandlingMethodException for the given request. + * @param request the offending HTTP request + */ + public NoSuchRequestHandlingMethodException(HttpServletRequest request) { + this(new UrlPathHelper().getRequestUri(request), request.getMethod(), request.getParameterMap()); + } + + /** + * Create a new NoSuchRequestHandlingMethodException. + * @param urlPath the request URI that has been used for handler lookup + * @param method the HTTP request method of the request + * @param parameterMap the request's parameters as map + */ + public NoSuchRequestHandlingMethodException(String urlPath, String method, Map parameterMap) { + super("No matching handler method found for servlet request: path '" + urlPath + + "', method '" + method + "', parameters " + StylerUtils.style(parameterMap)); + } + + /** + * Create a new NoSuchRequestHandlingMethodException for the given request. + * @param methodName the name of the handler method that wasn't found + * @param controllerClass the class the handler method was expected to be in + */ + public NoSuchRequestHandlingMethodException(String methodName, Class controllerClass) { + super("No request handling method with name '" + methodName + + "' in class [" + controllerClass.getName() + "]"); + this.methodName = methodName; + } + + + /** + * Return the name of the offending method, if known. + */ + public String getMethodName() { + return this.methodName; + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/multiaction/ParameterMethodNameResolver.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/multiaction/ParameterMethodNameResolver.java new file mode 100644 index 00000000000..6dfc33d1cdd --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/multiaction/ParameterMethodNameResolver.java @@ -0,0 +1,223 @@ +/* + * Copyright 2002-2006 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.web.servlet.mvc.multiaction; + +import java.util.Properties; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.web.util.WebUtils; + +/** + * Implementation of MethodNameResolver which supports several strategies for + * mapping parameter values to the names of methods to invoke. + * + *

The simplest strategy looks for a specific named parameter, whose value is + * considered the name of the method to invoke. The name of the parameter may be + * specified as a JavaBean property, if the default action is not + * acceptable. + * + *

The alternative strategy uses the very existence of a request parameter ( + * i.e. a request parameter with a certain name is found) as an indication that a + * method with the same name should be dispatched to. In this case, the actual + * request parameter value is ignored. The list of parameter/method names may + * be set via the methodParamNames JavaBean property. + * + *

The second resolution strategy is primarily expected to be used with web + * pages containing multiple submit buttons. The 'name' attribute of each + * button should be set to the mapped method name, while the 'value' attribute + * is normally displayed as the button label by the browser, and will be + * ignored by the resolver. + * + *

Note that the second strategy also supports the use of submit buttons of + * type 'image'. That is, an image submit button named 'reset' will normally be + * submitted by the browser as two request paramters called 'reset.x', and + * 'reset.y'. When checking for the existence of a paramter from the + * methodParamNames list, to indicate that a specific method should + * be called, the code will look for request parameter in the "reset" form + * (exactly as spcified in the list), and in the "reset.x" form ('.x' appended to + * the name in the list). In this way it can handle both normal and image submit + * buttons. The actual method name resolved if there is a match will always be + * the bare form without the ".x". + * + *

Note: If both strategies are configured, i.e. both "paramName" + * and "methodParamNames" are specified, then both will be checked for any given + * request. A match for an explicit request parameter in the "methodParamNames" + * list always wins over a value specified for a "paramName" action parameter. + * + *

For use with either strategy, the name of a default handler method to use + * when there is no match, can be specified as a JavaBean property. + * + *

For both resolution strategies, the method name is of course coming from + * some sort of view code, (such as a JSP page). While this may be acceptable, + * it is sometimes desireable to treat this only as a 'logical' method name, + * with a further mapping to a 'real' method name. As such, an optional + * 'logical' mapping may be specified for this purpose. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Colin Sampaleanu + * @see #setParamName + * @see #setMethodParamNames + * @see #setLogicalMappings + * @see #setDefaultMethodName + */ +public class ParameterMethodNameResolver implements MethodNameResolver { + + /** + * Default name for the parameter whose value identifies the method to invoke: + * "action". + */ + public static final String DEFAULT_PARAM_NAME = "action"; + + + protected final Log logger = LogFactory.getLog(getClass()); + + private String paramName = DEFAULT_PARAM_NAME; + + private String[] methodParamNames; + + private Properties logicalMappings; + + private String defaultMethodName; + + + /** + * Set the name of the parameter whose value identifies the name of + * the method to invoke. Default is "action". + *

Alternatively, specify parameter names where the very existence of each + * parameter means that a method of the same name should be invoked, via + * the "methodParamNames" property. + * @see #setMethodParamNames + */ + public void setParamName(String paramName) { + if (paramName != null) { + Assert.hasText(paramName, "'paramName' must not be empty"); + } + this.paramName = paramName; + } + + /** + * Set a String array of parameter names, where the very existence of a + * parameter in the list (with value ignored) means that a method of the + * same name should be invoked. This target method name may then be optionally + * further mapped via the {@link #logicalMappings} property, in which case it + * can be considered a logical name only. + * @see #setParamName + */ + public void setMethodParamNames(String[] methodParamNames) { + this.methodParamNames = methodParamNames; + } + + /** + * Specifies a set of optional logical method name mappings. For both resolution + * strategies, the method name initially comes in from the view layer. If that needs + * to be treated as a 'logical' method name, and mapped to a 'real' method name, then + * a name/value pair for that purpose should be added to this Properties instance. + * Any method name not found in this mapping will be considered to already be the + * real method name. + *

Note that in the case of no match, where the {@link #defaultMethodName} property + * is used if available, that method name is considered to already be the real method + * name, and is not run through the logical mapping. + * @param logicalMappings a Properties object mapping logical method names to real + * method names + */ + public void setLogicalMappings(Properties logicalMappings) { + this.logicalMappings = logicalMappings; + } + + /** + * Set the name of the default handler method that should be + * used when no parameter was found in the request + */ + public void setDefaultMethodName(String defaultMethodName) { + if (defaultMethodName != null) { + Assert.hasText(defaultMethodName, "'defaultMethodName' must not be empty"); + } + this.defaultMethodName = defaultMethodName; + } + + + public String getHandlerMethodName(HttpServletRequest request) throws NoSuchRequestHandlingMethodException { + String methodName = null; + + // Check parameter names where the very existence of each parameter + // means that a method of the same name should be invoked, if any. + if (this.methodParamNames != null) { + for (int i = 0; i < this.methodParamNames.length; ++i) { + String candidate = this.methodParamNames[i]; + if (WebUtils.hasSubmitParameter(request, candidate)) { + methodName = candidate; + if (logger.isDebugEnabled()) { + logger.debug("Determined handler method '" + methodName + + "' based on existence of explicit request parameter of same name"); + } + break; + } + } + } + + // Check parameter whose value identifies the method to invoke, if any. + if (methodName == null && this.paramName != null) { + methodName = request.getParameter(this.paramName); + if (methodName != null) { + if (logger.isDebugEnabled()) { + logger.debug("Determined handler method '" + methodName + + "' based on value of request parameter '" + this.paramName + "'"); + } + } + } + + if (methodName != null && this.logicalMappings != null) { + // Resolve logical name into real method name, if appropriate. + String originalName = methodName; + methodName = this.logicalMappings.getProperty(methodName, methodName); + if (logger.isDebugEnabled()) { + logger.debug("Resolved method name '" + originalName + "' to handler method '" + methodName + "'"); + } + } + + if (methodName != null && !StringUtils.hasText(methodName)) { + if (logger.isDebugEnabled()) { + logger.debug("Method name '" + methodName + "' is empty: treating it as no method name found"); + } + methodName = null; + } + + if (methodName == null) { + if (this.defaultMethodName != null) { + // No specific method resolved: use default method. + methodName = this.defaultMethodName; + if (logger.isDebugEnabled()) { + logger.debug("Falling back to default handler method '" + this.defaultMethodName + "'"); + } + } + else { + // If resolution failed completely, throw an exception. + throw new NoSuchRequestHandlingMethodException(request); + } + } + + return methodName; + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/multiaction/PropertiesMethodNameResolver.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/multiaction/PropertiesMethodNameResolver.java new file mode 100644 index 00000000000..fc3c1249bfc --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/multiaction/PropertiesMethodNameResolver.java @@ -0,0 +1,96 @@ +/* + * Copyright 2002-2005 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.web.servlet.mvc.multiaction; + +import java.util.Iterator; +import java.util.Properties; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.Assert; +import org.springframework.util.PathMatcher; + +/** + * The most flexible out-of-the-box implementation of the MethodNameResolver + * interface. Uses java.util.Properties to define the mapping + * between the URL of incoming requests and the corresponding method name. + * Such properties can be held in an XML document. + * + *

Properties format is + * + * /welcome.html=displayGenresPage + * + * Note that method overloading isn't allowed, so there's no need to + * specify arguments. + * + *

Supports direct matches, e.g. a registered "/test" matches "/test", + * and a various Ant-style pattern matches, e.g. a registered "/t*" matches + * both "/test" and "/team". For details, see the AntPathMatcher javadoc. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see java.util.Properties + * @see org.springframework.util.AntPathMatcher + */ +public class PropertiesMethodNameResolver extends AbstractUrlMethodNameResolver + implements InitializingBean { + + private Properties mappings; + + private PathMatcher pathMatcher = new AntPathMatcher(); + + + /** + * Set explicit URL to method name mappings through a Properties object. + * @param mappings Properties with URL as key and method name as value + */ + public void setMappings(Properties mappings) { + this.mappings = mappings; + } + + /** + * Set the PathMatcher implementation to use for matching URL paths + * against registered URL patterns. Default is AntPathMatcher. + * @see org.springframework.util.AntPathMatcher + */ + public void setPathMatcher(PathMatcher pathMatcher) { + Assert.notNull(pathMatcher, "PathMatcher must not be null"); + this.pathMatcher = pathMatcher; + } + + public void afterPropertiesSet() { + if (this.mappings == null || this.mappings.isEmpty()) { + throw new IllegalArgumentException("'mappings' property is required"); + } + } + + + protected String getHandlerMethodNameForUrlPath(String urlPath) { + String methodName = this.mappings.getProperty(urlPath); + if (methodName != null) { + return methodName; + } + for (Iterator it = this.mappings.keySet().iterator(); it.hasNext();) { + String registeredPath = (String) it.next(); + if (this.pathMatcher.match(registeredPath, urlPath)) { + return (String) this.mappings.get(registeredPath); + } + } + return null; + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/multiaction/package.html b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/multiaction/package.html new file mode 100644 index 00000000000..b6733816808 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/multiaction/package.html @@ -0,0 +1,23 @@ + + + +Package allowing MVC Controller implementations to handle requests +at method rather than class level. This is useful when +we want to avoid having many trivial controller classes, as can +easily happen when using an MVC framework. + +

Typically a controller that handles multiple request types will +extend MultiActionController, and implement multiple request handling +methods that will be invoked by reflection if they follow this class' +naming convention. Classes are analyzed at startup and methods cached, +so the performance overhead of reflection in this approach is negligible. + +

This approach is analogous to the Struts 1.1 DispatcherAction +class, but more sophisticated, as it supports configurable mapping from +requests to URLs and allows for delegation as well as subclassing. + +

This package is discussed in Chapter 12 of Expert One-On-One J2EE Design and Development +by Rod Johnson, and used in the sample application. + + + diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/package.html b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/package.html new file mode 100644 index 00000000000..b2cb2449941 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/package.html @@ -0,0 +1,44 @@ + + + +

+ Standard controller implementations for the servlet MVC framework that comes + with Spring. Provides both abstract base classes and concrete implementations + for often seen use cases. +

+ +

+ A Controller - as defined in this package - is analogous to a Struts + Action. Usually Controllers are JavaBeans + to allow easy configuration using the {@link org.springframework.beans org.springframework.beans} + package. Controllers define the C from so-called MVC paradigm + and can be used in conjunction with the {@link org.springframework.web.servlet.ModelAndView ModelAndView} + to achieve interactive applications. The view might be represented by a + HTML interface, but, because of model and the controller being completely + independent of the view, PDF views are possible, as well as for instance Excel + views. +

+ +

+ How to actually set up a (web)application using the MVC framework Spring + provides is explained in more detail in the + MVC-Step-by-Step + tutorial, also provided in this package (or have a look + here for an online version). + The classes contained by this package explain in more detail the actual + workflow of some of the abstract and concrete controller and how to extend + and fully use their functionality. +

+ +

+ Especially useful to read, while getting into the Spring MVC framework + are the following: +

+

+ + + diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/support/AbstractControllerUrlHandlerMapping.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/support/AbstractControllerUrlHandlerMapping.java new file mode 100644 index 00000000000..937cb35f8aa --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/support/AbstractControllerUrlHandlerMapping.java @@ -0,0 +1,183 @@ +/* + * Copyright 2002-2008 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.web.servlet.mvc.support; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import org.springframework.core.JdkVersion; +import org.springframework.util.ClassUtils; +import org.springframework.web.servlet.handler.AbstractDetectingUrlHandlerMapping; + +/** + * Base class for {@link org.springframework.web.servlet.HandlerMapping} implementations + * that derive URL paths according to conventions for specific controller types. + * + * @author Juergen Hoeller + * @since 2.5.3 + * @see ControllerClassNameHandlerMapping + * @see ControllerBeanNameHandlerMapping + */ +public abstract class AbstractControllerUrlHandlerMapping extends AbstractDetectingUrlHandlerMapping { + + private static final String ANNOTATION_PREDICATE_NAME = + "org.springframework.web.servlet.mvc.support.AnnotationControllerTypePredicate"; + + private ControllerTypePredicate predicate; + + private Set excludedPackages = Collections.singleton("org.springframework.web.servlet.mvc"); + + private Set excludedClasses = Collections.EMPTY_SET; + + + /** + * Activates detection of annotated controllers when running on JDK 1.5 or higher. + */ + public AbstractControllerUrlHandlerMapping() { + this.predicate = (JdkVersion.isAtLeastJava15() ? + instantiateAnnotationPredicate() : new ControllerTypePredicate()); + } + + + /** + * Set whether to activate or deactivate detection of annotated controllers. + *

Annotated controllers will by included by default when runnong on JDK 1.5 or higher. + */ + public void setIncludeAnnotatedControllers(boolean includeAnnotatedControllers) { + this.predicate = (includeAnnotatedControllers ? + instantiateAnnotationPredicate() : new ControllerTypePredicate()); + } + + /** + * Specify Java packages that should be excluded from this mapping. + * Any classes in such a package (or any of its subpackages) will be + * ignored by this HandlerMapping. + *

Default is to exclude the entire "org.springframework.web.servlet.mvc" + * package, including its subpackages, since none of Spring's out-of-the-box + * Controller implementations is a reasonable candidate for this mapping strategy. + * Such controllers are typically handled by a separate HandlerMapping, + * e.g. a {@link org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping}, + * alongside this ControllerClassNameHandlerMapping for application controllers. + */ + public void setExcludedPackages(String[] excludedPackages) { + this.excludedPackages = + (excludedPackages != null ? new HashSet(Arrays.asList(excludedPackages)) : Collections.EMPTY_SET); + } + + /** + * Specify controller classes that should be excluded from this mapping. + * Any such classes will simply be ignored by this HandlerMapping. + */ + public void setExcludedClasses(Class[] excludedClasses) { + this.excludedClasses = + (excludedClasses != null ? new HashSet(Arrays.asList(excludedClasses)) : Collections.EMPTY_SET); + } + + + private ControllerTypePredicate instantiateAnnotationPredicate() { + try { + return (ControllerTypePredicate) ClassUtils.forName(ANNOTATION_PREDICATE_NAME, + AbstractControllerUrlHandlerMapping.class.getClassLoader()).newInstance(); + } + catch (Exception ex) { + throw new IllegalStateException("Cannot load AnnotationControllerTypePredicate", ex); + } + } + + + /** + * This implementation delegates to {@link #buildUrlsForHandler}, + * provided that {@link #isEligibleForMapping} returns true. + */ + protected String[] determineUrlsForHandler(String beanName) { + Class beanClass = getApplicationContext().getType(beanName); + if (isEligibleForMapping(beanName, beanClass)) { + return buildUrlsForHandler(beanName, beanClass); + } + else { + return null; + } + } + + /** + * Determine whether the specified controller is excluded from this mapping. + * @param beanName the name of the controller bean + * @param beanClass the concrete class of the controller bean + * @return whether the specified class is excluded + * @see #setExcludedPackages + * @see #setExcludedClasses + */ + protected boolean isEligibleForMapping(String beanName, Class beanClass) { + if (beanClass == null) { + if (logger.isDebugEnabled()) { + logger.debug("Excluding controller bean '" + beanName + "' from class name mapping " + + "because its bean type could not be determined"); + } + return false; + } + if (this.excludedClasses.contains(beanClass)) { + if (logger.isDebugEnabled()) { + logger.debug("Excluding controller bean '" + beanName + "' from class name mapping " + + "because its bean class is explicitly excluded: " + beanClass.getName()); + } + return false; + } + String beanClassName = beanClass.getName(); + for (Iterator it = this.excludedPackages.iterator(); it.hasNext();) { + String packageName = (String) it.next(); + if (beanClassName.startsWith(packageName)) { + if (logger.isDebugEnabled()) { + logger.debug("Excluding controller bean '" + beanName + "' from class name mapping " + + "because its bean class is defined in an excluded package: " + beanClass.getName()); + } + return false; + } + } + return isControllerType(beanClass); + } + + /** + * Determine whether the given bean class indicates a controller type + * that is supported by this mapping strategy. + * @param beanClass the class to introspect + */ + protected boolean isControllerType(Class beanClass) { + return this.predicate.isControllerType(beanClass); + } + + /** + * Determine whether the given bean class indicates a controller type + * that dispatches to multiple action methods. + * @param beanClass the class to introspect + */ + protected boolean isMultiActionControllerType(Class beanClass) { + return this.predicate.isMultiActionControllerType(beanClass); + } + + + /** + * Abstract template method to be implemented by subclasses. + * @param beanName the name of the bean + * @param beanClass the type of the bean + * @return the URLs determined for the bean + */ + protected abstract String[] buildUrlsForHandler(String beanName, Class beanClass); + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/support/AnnotationControllerTypePredicate.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/support/AnnotationControllerTypePredicate.java new file mode 100644 index 00000000000..3d6c7bc1e60 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/support/AnnotationControllerTypePredicate.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2008 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.web.servlet.mvc.support; + +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.stereotype.Controller; + +/** + * Extension of {@link ControllerTypePredicate} that detects + * annotated @Controller beans as well. + * + * @author Juergen Hoeller + * @since 2.5.3 + */ +class AnnotationControllerTypePredicate extends ControllerTypePredicate { + + public boolean isControllerType(Class beanClass) { + return (super.isControllerType(beanClass) || + AnnotationUtils.findAnnotation(beanClass, Controller.class) != null); + } + + public boolean isMultiActionControllerType(Class beanClass) { + return (super.isMultiActionControllerType(beanClass) || + AnnotationUtils.findAnnotation(beanClass, Controller.class) != null); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/support/ControllerBeanNameHandlerMapping.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/support/ControllerBeanNameHandlerMapping.java new file mode 100644 index 00000000000..7c5ae10de47 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/support/ControllerBeanNameHandlerMapping.java @@ -0,0 +1,94 @@ +/* + * Copyright 2002-2008 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.web.servlet.mvc.support; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.util.StringUtils; + +/** + * Implementation of {@link org.springframework.web.servlet.HandlerMapping} that + * follows a simple convention for generating URL path mappings from the bean names + * of registered {@link org.springframework.web.servlet.mvc.Controller} and + * {@link org.springframework.web.servlet.mvc.throwaway.ThrowawayController} beans + * as well as @Controller annotated beans. + * + *

This is similar to {@link org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping} + * but doesn't expect bean names to follow the URL convention: It turns plain bean names + * into URLs by prepending a slash and optionally applying a specified prefix and/or suffix. + * However, it only does so for well-known {@link #isControllerType controller types}, + * as listed above (analogous to {@link ControllerClassNameHandlerMapping}). + * + * @author Juergen Hoeller + * @since 2.5.3 + * @see ControllerClassNameHandlerMapping + * @see org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping + */ +public class ControllerBeanNameHandlerMapping extends AbstractControllerUrlHandlerMapping { + + private String urlPrefix = ""; + + private String urlSuffix = ""; + + + /** + * Set an optional prefix to prepend to generated URL mappings. + *

By default this is an empty String. If you want a prefix like + * "/myapp/", you can set it for all beans mapped by this mapping. + */ + public void setUrlPrefix(String urlPrefix) { + this.urlPrefix = (urlPrefix != null ? urlPrefix : ""); + } + + /** + * Set an optional suffix to append to generated URL mappings. + *

By default this is an empty String. If you want a suffix like + * ".do", you can set it for all beans mapped by this mapping. + */ + public void setUrlSuffix(String urlSuffix) { + this.urlSuffix = (urlSuffix != null ? urlSuffix : ""); + } + + + protected String[] buildUrlsForHandler(String beanName, Class beanClass) { + List urls = new ArrayList(); + urls.add(generatePathMapping(beanName)); + String[] aliases = getApplicationContext().getAliases(beanName); + for (int i = 0; i < aliases.length; i++) { + urls.add(generatePathMapping(aliases[i])); + } + return StringUtils.toStringArray(urls); + } + + /** + * Prepends a '/' if required and appends the URL suffix to the name. + */ + protected String generatePathMapping(String beanName) { + String name = (beanName.startsWith("/") ? beanName : "/" + beanName); + StringBuffer path = new StringBuffer(); + if (!name.startsWith(this.urlPrefix)) { + path.append(this.urlPrefix); + } + path.append(name); + if (!name.endsWith(this.urlSuffix)) { + path.append(this.urlSuffix); + } + return path.toString(); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/support/ControllerClassNameHandlerMapping.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/support/ControllerClassNameHandlerMapping.java new file mode 100644 index 00000000000..ac52ebeb1a6 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/support/ControllerClassNameHandlerMapping.java @@ -0,0 +1,183 @@ +/* + * Copyright 2002-2008 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.web.servlet.mvc.support; + +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.servlet.mvc.multiaction.MultiActionController; + +/** + * Implementation of {@link org.springframework.web.servlet.HandlerMapping} that + * follows a simple convention for generating URL path mappings from the class names + * of registered {@link org.springframework.web.servlet.mvc.Controller} and + * {@link org.springframework.web.servlet.mvc.throwaway.ThrowawayController} beans + * as well as @Controller annotated beans. + * + *

For simple {@link org.springframework.web.servlet.mvc.Controller} implementations + * (those that handle a single request type), the convention is to take the + * {@link ClassUtils#getShortName short name} of the Class, + * remove the 'Controller' suffix if it exists and return the remaining text, lower-cased, + * as the mapping, with a leading /. For example: + *

    + *
  • WelcomeController -> /welcome*
  • + *
  • HomeController -> /home*
  • + *
+ * + *

For {@link MultiActionController MultiActionControllers} and @Controller + * beans, a similar mapping is registered, except that all sub-paths are registered + * using the trailing wildcard pattern /*. For example: + *

    + *
  • WelcomeController -> /welcome, /welcome/*
  • + *
  • CatalogController -> /catalog, /catalog/*
  • + *
+ * + *

For {@link MultiActionController} it is often useful to use + * this mapping strategy in conjunction with the + * {@link org.springframework.web.servlet.mvc.multiaction.InternalPathMethodNameResolver}. + * + *

Thanks to Warren Oliver for suggesting the "caseSensitive", "pathPrefix" + * and "basePackage" properties which have been added in Spring 2.5. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + * @see org.springframework.web.servlet.mvc.Controller + * @see org.springframework.web.servlet.mvc.throwaway.ThrowawayController + * @see org.springframework.web.servlet.mvc.multiaction.MultiActionController + */ +public class ControllerClassNameHandlerMapping extends AbstractControllerUrlHandlerMapping { + + /** + * Common suffix at the end of controller implementation classes. + * Removed when generating the URL path. + */ + private static final String CONTROLLER_SUFFIX = "Controller"; + + + private boolean caseSensitive = false; + + private String pathPrefix; + + private String basePackage; + + + /** + * Set whether to apply case sensitivity to the generated paths, + * e.g. turning the class name "BuyForm" into "buyForm". + *

Default is "false", using pure lower case paths, + * e.g. turning the class name "BuyForm" into "buyform". + */ + public void setCaseSensitive(boolean caseSensitive) { + this.caseSensitive = caseSensitive; + } + + /** + * Specify a prefix to prepend to the path generated from the controller name. + *

Default is a plain slash ("/"). A path like "/mymodule" can be specified + * in order to have controller path mappings prefixed with that path, e.g. + * "/mymodule/buyform" instead of "/buyform" for the class name "BuyForm". + */ + public void setPathPrefix(String prefixPath) { + this.pathPrefix = prefixPath; + if (StringUtils.hasLength(this.pathPrefix)) { + if (!this.pathPrefix.startsWith("/")) { + this.pathPrefix = "/" + this.pathPrefix; + } + if (this.pathPrefix.endsWith("/")) { + this.pathPrefix = this.pathPrefix.substring(0, this.pathPrefix.length() - 1); + } + } + } + + /** + * Set the base package to be used for generating path mappings, + * including all subpackages underneath this packages as path elements. + *

Default is null, using the short class name for the + * generated path, with the controller's package not represented in the path. + * Specify a base package like "com.mycompany.myapp" to include subpackages + * within that base package as path elements, e.g. generating the path + * "/mymodule/buyform" for the class name "com.mycompany.myapp.mymodule.BuyForm". + * Subpackage hierarchies are represented as individual path elements, + * e.g. "/mymodule/mysubmodule/buyform" for the class name + * "com.mycompany.myapp.mymodule.mysubmodule.BuyForm". + */ + public void setBasePackage(String basePackage) { + this.basePackage = basePackage; + if (StringUtils.hasLength(this.basePackage) && !this.basePackage.endsWith(".")) { + this.basePackage = this.basePackage + "."; + } + } + + + protected String[] buildUrlsForHandler(String beanName, Class beanClass) { + return generatePathMappings(beanClass); + } + + /** + * Generate the actual URL paths for the given controller class. + *

Subclasses may choose to customize the paths that are generated + * by overriding this method. + * @param beanClass the controller bean class to generate a mapping for + * @return the URL path mappings for the given controller + */ + protected String[] generatePathMappings(Class beanClass) { + StringBuffer pathMapping = buildPathPrefix(beanClass); + String className = ClassUtils.getShortName(beanClass); + String path = (className.endsWith(CONTROLLER_SUFFIX) ? + className.substring(0, className.indexOf(CONTROLLER_SUFFIX)) : className); + if (path.length() > 0) { + if (this.caseSensitive) { + pathMapping.append(path.substring(0, 1).toLowerCase()).append(path.substring(1)); + } + else { + pathMapping.append(path.toLowerCase()); + } + } + if (isMultiActionControllerType(beanClass)) { + return new String[] {pathMapping.toString(), pathMapping.toString() + "/*"}; + } + else { + return new String[] {pathMapping.toString() + "*"}; + } + } + + /** + * Build a path prefix for the given controller bean class. + * @param beanClass the controller bean class to generate a mapping for + * @return the path prefix, potentially including subpackage names as path elements + */ + private StringBuffer buildPathPrefix(Class beanClass) { + StringBuffer pathMapping = new StringBuffer(); + if (this.pathPrefix != null) { + pathMapping.append(this.pathPrefix); + pathMapping.append("/"); + } + else { + pathMapping.append("/"); + } + if (this.basePackage != null) { + String packageName = ClassUtils.getPackageName(beanClass); + if (packageName.startsWith(this.basePackage)) { + String subPackage = packageName.substring(this.basePackage.length()).replace('.', '/'); + pathMapping.append(this.caseSensitive ? subPackage : subPackage.toLowerCase()); + pathMapping.append("/"); + } + } + return pathMapping; + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/support/ControllerTypePredicate.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/support/ControllerTypePredicate.java new file mode 100644 index 00000000000..50fcba66092 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/support/ControllerTypePredicate.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2008 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.web.servlet.mvc.support; + +import org.springframework.web.servlet.mvc.Controller; +import org.springframework.web.servlet.mvc.multiaction.MultiActionController; +import org.springframework.web.servlet.mvc.throwaway.ThrowawayController; + +/** + * Internal helper class that identifies controller types. + * + * @author Juergen Hoeller + * @since 2.5.3 + */ +class ControllerTypePredicate { + + public boolean isControllerType(Class beanClass) { + return (Controller.class.isAssignableFrom(beanClass) || + ThrowawayController.class.isAssignableFrom(beanClass)); + } + + public boolean isMultiActionControllerType(Class beanClass) { + return (MultiActionController.class.isAssignableFrom(beanClass)); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/support/package.html b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/support/package.html new file mode 100644 index 00000000000..fbfff2e420a --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/support/package.html @@ -0,0 +1,8 @@ + + + +Support package for MVC controllers. +Contains a special HandlerMapping for controller conventions. + + + diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/throwaway/ThrowawayController.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/throwaway/ThrowawayController.java new file mode 100644 index 00000000000..e1c295af937 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/throwaway/ThrowawayController.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2008 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.web.servlet.mvc.throwaway; + +import org.springframework.web.servlet.ModelAndView; + +/** + * ThrowawayController is an alternative to Spring's default Controller interface, + * for executable per-request command instances that are not aware of the Servlet API. + * In contrast to Controller, implementing beans are not supposed to be defined as + * Servlet/Struts-style singletons that process a HttpServletRequest but rather as + * WebWork/Maverick-style prototypes that get populated with request parameters, + * executed to determine a view, and thrown away afterwards. + * + *

The main advantage of this controller programming model is that controllers + * are testable without HttpServletRequest/HttpServletResponse mocks, just like + * WebWork actions. They are still web UI workflow controllers: Spring does not + * aim for the arguably hard-to-achieve reusability of such controllers in non-web + * environments, as XWork (the generic command framework from WebWork2) does + * but just for ease of testing. + * + *

A ThrowawayController differs from the command notion of Base- or + * AbstractCommandController in that a ThrowawayController is an executable + * command that contains workflow logic to determine the next view to render, + * while BaseCommandController treats commands as plain parameter holders. + * + *

If binding request parameters to this controller fails, a fatal BindException + * will be thrown. + * + *

If you need access to the HttpServletRequest and/or HttpServletResponse, + * consider implementing Controller or deriving from AbstractCommandController. + * ThrowawayController is specifically intended for controllers that are not aware + * of the Servlet API at all. Accordingly, if you need to handle session form objects + * or even wizard forms, consider the corresponding Controller subclasses. + * + * @author Juergen Hoeller + * @since 08.12.2003 + * @see org.springframework.web.servlet.mvc.Controller + * @see org.springframework.web.servlet.mvc.AbstractCommandController + * @deprecated as of Spring 2.5, in favor of annotation-based controllers. + * To be removed in Spring 3.0. + */ +public interface ThrowawayController { + + /** + * Execute this controller according to its bean properties. + * Gets invoked after a new instance of the controller has been populated with request + * parameters. Is supposed to return a ModelAndView in any case, as it is not able to + * generate a response itself. + * @return a ModelAndView to render + * @throws Exception in case of errors + */ + ModelAndView execute() throws Exception; + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/throwaway/ThrowawayControllerHandlerAdapter.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/throwaway/ThrowawayControllerHandlerAdapter.java new file mode 100644 index 00000000000..a6a5dda7a35 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/throwaway/ThrowawayControllerHandlerAdapter.java @@ -0,0 +1,134 @@ +/* + * Copyright 2002-2008 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.web.servlet.mvc.throwaway; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.web.bind.ServletRequestDataBinder; +import org.springframework.web.servlet.HandlerAdapter; +import org.springframework.web.servlet.ModelAndView; + +/** + * Adapter to use the ThrowawayController workflow interface with the + * generic DispatcherServlet. Does not support last-modified checks. + * + *

This is an SPI class, not used directly by application code. + * It can be explicitly configured in a DispatcherServlet context, to use a + * customized version instead of the default ThrowawayControllerHandlerAdapter. + * + * @author Juergen Hoeller + * @since 08.12.2003 + * @deprecated as of Spring 2.5, in favor of annotation-based controllers. + * To be removed in Spring 3.0. + */ +public class ThrowawayControllerHandlerAdapter implements HandlerAdapter { + + public static final String DEFAULT_COMMAND_NAME = "throwawayController"; + + private String commandName = DEFAULT_COMMAND_NAME; + + + /** + * Set the name of the command in the model. + * The command object will be included in the model under this name. + */ + public final void setCommandName(String commandName) { + this.commandName = commandName; + } + + /** + * Return the name of the command in the model. + */ + public final String getCommandName() { + return this.commandName; + } + + + public boolean supports(Object handler) { + return (handler instanceof ThrowawayController); + } + + + /** + * This implementation binds request parameters to the ThrowawayController + * instance and then calls execute on it. + * @see #createBinder + * @see ThrowawayController#execute + */ + public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + + ThrowawayController throwaway = (ThrowawayController) handler; + + ServletRequestDataBinder binder = createBinder(request, throwaway); + binder.bind(request); + binder.closeNoCatch(); + + return throwaway.execute(); + } + + /** + * Create a new binder instance for the given command and request. + *

Called by bindAndValidate. Can be overridden to plug in + * custom ServletRequestDataBinder subclasses. + *

Default implementation creates a standard ServletRequestDataBinder, + * sets the specified MessageCodesResolver (if any), and invokes initBinder. + * Note that initBinder will not be invoked if you override this method! + * @param request current HTTP request + * @param command the command to bind onto + * @return the new binder instance + * @throws Exception in case of invalid state or arguments + * @see #initBinder + * @see #getCommandName + */ + protected ServletRequestDataBinder createBinder(HttpServletRequest request, ThrowawayController command) + throws Exception { + + ServletRequestDataBinder binder = new ServletRequestDataBinder(command, getCommandName()); + initBinder(request, binder); + return binder; + } + + /** + * Initialize the given binder instance, for example with custom editors. + * Called by createBinder. + *

This method allows you to register custom editors for certain fields of your + * command class. For instance, you will be able to transform Date objects into a + * String pattern and back, in order to allow your JavaBeans to have Date properties + * and still be able to set and display them in an HTML interface. + *

Default implementation is empty. + * @param request current HTTP request + * @param binder new binder instance + * @throws Exception in case of invalid state or arguments + * @see #createBinder + * @see org.springframework.validation.DataBinder#registerCustomEditor + * @see org.springframework.beans.propertyeditors.CustomDateEditor + */ + protected void initBinder(HttpServletRequest request, ServletRequestDataBinder binder) + throws Exception { + } + + + /** + * This implementation always returns -1, as last-modified checks are not supported. + */ + public long getLastModified(HttpServletRequest request, Object handler) { + return -1; + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/throwaway/package.html b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/throwaway/package.html new file mode 100644 index 00000000000..c8edb529dd2 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/throwaway/package.html @@ -0,0 +1,8 @@ + + + +Throwaway command controllers are a WebWork/Maverick-style alternative +to Spring's default Servlet/Struts-style Controller approach. + + + diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/package.html b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/package.html new file mode 100644 index 00000000000..4231de0612c --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/package.html @@ -0,0 +1,13 @@ + + + +Provides servlets that integrate with the application context +infrastructure, and the core interfaces and classes for the +Spring web MVC framework. + +

This package and related packages are discussed in Chapters 12 and 13 of +Expert One-On-One J2EE Design and Development +by Rod Johnson (Wrox, 2002). + + + diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/support/BindStatus.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/support/BindStatus.java new file mode 100644 index 00000000000..12375213075 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/support/BindStatus.java @@ -0,0 +1,336 @@ +/* + * Copyright 2002-2008 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.web.servlet.support; + +import java.beans.PropertyEditor; +import java.util.Arrays; +import java.util.List; + +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.PropertyAccessorFactory; +import org.springframework.context.NoSuchMessageException; +import org.springframework.util.StringUtils; +import org.springframework.validation.BindingResult; +import org.springframework.validation.Errors; +import org.springframework.validation.ObjectError; +import org.springframework.web.util.HtmlUtils; + +/** + * Simple adapter to expose the bind status of a field or object. + * Set as a variable both by the JSP bind tag and Velocity/FreeMarker macros. + * + *

Obviously, object status representations (i.e. errors at the object level + * rather than the field level) do not have an expression and a value but only + * error codes and messages. For simplicity's sake and to be able to use the same + * tags and macros, the same status class is used for both scenarios. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Darren Davison + * @see RequestContext#getBindStatus + * @see org.springframework.web.servlet.tags.BindTag + * @see org.springframework.web.servlet.view.AbstractTemplateView#setExposeSpringMacroHelpers + */ +public class BindStatus { + + private final RequestContext requestContext; + + private final String path; + + private final boolean htmlEscape; + + private final String expression; + + private final Errors errors; + + private BindingResult bindingResult; + + private Object value; + + private Class valueType; + + private Object actualValue; + + private PropertyEditor editor; + + private List objectErrors; + + private String[] errorCodes; + + private String[] errorMessages; + + + /** + * Create a new BindStatus instance, representing a field or object status. + * @param requestContext the current RequestContext + * @param path the bean and property path for which values and errors + * will be resolved (e.g. "customer.address.street") + * @param htmlEscape whether to HTML-escape error messages and string values + * @throws IllegalStateException if no corresponding Errors object found + */ + public BindStatus(RequestContext requestContext, String path, boolean htmlEscape) + throws IllegalStateException { + + this.requestContext = requestContext; + this.path = path; + this.htmlEscape = htmlEscape; + + // determine name of the object and property + String beanName = null; + int dotPos = path.indexOf('.'); + if (dotPos == -1) { + // property not set, only the object itself + beanName = path; + this.expression = null; + } + else { + beanName = path.substring(0, dotPos); + this.expression = path.substring(dotPos + 1); + } + + this.errors = requestContext.getErrors(beanName, false); + + if (this.errors != null) { + // Usual case: A BindingResult is available as request attribute. + // Can determine error codes and messages for the given expression. + // Can use a custom PropertyEditor, as registered by a form controller. + if (this.expression != null) { + if ("*".equals(this.expression)) { + this.objectErrors = this.errors.getAllErrors(); + } + else if (this.expression.endsWith("*")) { + this.objectErrors = this.errors.getFieldErrors(this.expression); + } + else { + this.objectErrors = this.errors.getFieldErrors(this.expression); + this.value = this.errors.getFieldValue(this.expression); + this.valueType = this.errors.getFieldType(this.expression); + if (this.errors instanceof BindingResult) { + this.bindingResult = (BindingResult) this.errors; + this.actualValue = this.bindingResult.getRawFieldValue(this.expression); + this.editor = this.bindingResult.findEditor(this.expression, null); + } + } + } + else { + this.objectErrors = this.errors.getGlobalErrors(); + } + initErrorCodes(); + } + + else { + // No BindingResult available as request attribute: + // Probably forwarded directly to a form view. + // Let's do the best we can: extract a plain target if appropriate. + Object target = requestContext.getModelObject(beanName); + if (target == null) { + throw new IllegalStateException("Neither BindingResult nor plain target object for bean name '" + + beanName + "' available as request attribute"); + } + if (this.expression != null && !"*".equals(this.expression) && !this.expression.endsWith("*")) { + BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(target); + this.valueType = bw.getPropertyType(this.expression); + this.value = bw.getPropertyValue(this.expression); + } + this.errorCodes = new String[0]; + this.errorMessages = new String[0]; + } + + if (htmlEscape && this.value instanceof String) { + this.value = HtmlUtils.htmlEscape((String) this.value); + } + } + + /** + * Extract the error codes from the ObjectError list. + */ + private void initErrorCodes() { + this.errorCodes = new String[this.objectErrors.size()]; + for (int i = 0; i < this.objectErrors.size(); i++) { + ObjectError error = (ObjectError) this.objectErrors.get(i); + this.errorCodes[i] = error.getCode(); + } + } + + /** + * Extract the error messages from the ObjectError list. + */ + private void initErrorMessages() throws NoSuchMessageException { + if (this.errorMessages == null) { + this.errorMessages = new String[this.objectErrors.size()]; + for (int i = 0; i < this.objectErrors.size(); i++) { + ObjectError error = (ObjectError) this.objectErrors.get(i); + this.errorMessages[i] = this.requestContext.getMessage(error, this.htmlEscape); + } + } + } + + + /** + * Return the bean and property path for which values and errors + * will be resolved (e.g. "customer.address.street"). + */ + public String getPath() { + return this.path; + } + + /** + * Return a bind expression that can be used in HTML forms as input name + * for the respective field, or null if not field-specific. + *

Returns a bind path appropriate for resubmission, e.g. "address.street". + * Note that the complete bind path as required by the bind tag is + * "customer.address.street", if bound to a "customer" bean. + */ + public String getExpression() { + return this.expression; + } + + /** + * Return the current value of the field, i.e. either the property value + * or a rejected update, or null if not field-specific. + *

This value will be an HTML-escaped String if the original value + * already was a String. + */ + public Object getValue() { + return this.value; + } + + /** + * Get the 'Class' type of the field. Favor this instead of + * 'getValue().getClass()' since 'getValue()' may + * return 'null'. + */ + public Class getValueType() { + return this.valueType; + } + + /** + * Return the actual value of the field, i.e. the raw property value, + * or null if not available. + */ + public Object getActualValue() { + return this.actualValue; + } + + /** + * Return a suitable display value for the field, i.e. the stringified + * value if not null, and an empty string in case of a null value. + *

This value will be an HTML-escaped String if the original value + * was non-null: the toString result of the original value + * will get HTML-escaped. + */ + public String getDisplayValue() { + if (this.value instanceof String) { + return (String) this.value; + } + if (this.value != null) { + return (this.htmlEscape ? HtmlUtils.htmlEscape(this.value.toString()) : this.value.toString()); + } + return ""; + } + + /** + * Return if this status represents a field or object error. + */ + public boolean isError() { + return (this.errorCodes != null && this.errorCodes.length > 0); + } + + /** + * Return the error codes for the field or object, if any. + * Returns an empty array instead of null if none. + */ + public String[] getErrorCodes() { + return this.errorCodes; + } + + /** + * Return the first error codes for the field or object, if any. + */ + public String getErrorCode() { + return (this.errorCodes.length > 0 ? this.errorCodes[0] : ""); + } + + /** + * Return the resolved error messages for the field or object, + * if any. Returns an empty array instead of null if none. + */ + public String[] getErrorMessages() { + initErrorMessages(); + return this.errorMessages; + } + + /** + * Return the first error message for the field or object, if any. + */ + public String getErrorMessage() { + initErrorMessages(); + return (this.errorMessages.length > 0 ? this.errorMessages[0] : ""); + } + + /** + * Return an error message string, concatenating all messages + * separated by the given delimiter. + * @param delimiter separator string, e.g. ", " or "
" + * @return the error message string + */ + public String getErrorMessagesAsString(String delimiter) { + initErrorMessages(); + return StringUtils.arrayToDelimitedString(this.errorMessages, delimiter); + } + + /** + * Return the Errors instance (typically a BindingResult) that this + * bind status is currently associated with. + * @return the current Errors instance, or null if none + * @see org.springframework.validation.BindingResult + */ + public Errors getErrors() { + return this.errors; + } + + /** + * Return the PropertyEditor for the property that this bind status + * is currently bound to. + * @return the current PropertyEditor, or null if none + */ + public PropertyEditor getEditor() { + return this.editor; + } + + /** + * Find a PropertyEditor for the given value class, associated with + * the property that this bound status is currently bound to. + * @param valueClass the value class that an editor is needed for + * @return the associated PropertyEditor, or null if none + */ + public PropertyEditor findEditor(Class valueClass) { + return (this.bindingResult != null ? this.bindingResult.findEditor(this.expression, valueClass) : null); + } + + + public String toString() { + StringBuffer sb = new StringBuffer("BindStatus: "); + sb.append("expression=[").append(this.expression).append("]; "); + sb.append("value=[").append(this.value).append("]"); + if (isError()) { + sb.append("; errorCodes=").append(Arrays.asList(this.errorCodes)); + } + return sb.toString(); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/support/JspAwareRequestContext.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/support/JspAwareRequestContext.java new file mode 100644 index 00000000000..7126f51dada --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/support/JspAwareRequestContext.java @@ -0,0 +1,116 @@ +/* + * Copyright 2002-2007 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.web.servlet.support; + +import java.util.Locale; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.jsp.PageContext; +import javax.servlet.jsp.jstl.core.Config; + +import org.springframework.util.ClassUtils; + +/** + * JSP-aware (and JSTL-aware) subclass of RequestContext, allowing for + * population of the context from a javax.servlet.jsp.PageContext. + * + *

This context will detect a JSTL locale attribute in page/request/session/application + * scope, in addition to the fallback locale strategy provided by the base class. + * + * @author Juergen Hoeller + * @since 1.1.4 + * @see #getFallbackLocale + */ +public class JspAwareRequestContext extends RequestContext { + + private PageContext pageContext; + + + /** + * Create a new JspAwareRequestContext for the given page context, + * using the request attributes for Errors retrieval. + * @param pageContext current JSP page context + */ + public JspAwareRequestContext(PageContext pageContext) { + initContext(pageContext, null); + } + + /** + * Create a new JspAwareRequestContext for the given page context, + * using the given model attributes for Errors retrieval. + * @param pageContext current JSP page context + * @param model the model attributes for the current view + * (can be null, using the request attributes for Errors retrieval) + */ + public JspAwareRequestContext(PageContext pageContext, Map model) { + initContext(pageContext, model); + } + + /** + * Initialize this context with the given page context, + * using the given model attributes for Errors retrieval. + * @param pageContext current JSP page context + * @param model the model attributes for the current view + * (can be null, using the request attributes for Errors retrieval) + */ + protected void initContext(PageContext pageContext, Map model) { + if (!(pageContext.getRequest() instanceof HttpServletRequest)) { + throw new IllegalArgumentException("RequestContext only supports HTTP requests"); + } + this.pageContext = pageContext; + initContext((HttpServletRequest) pageContext.getRequest(), pageContext.getServletContext(), model); + } + + + /** + * Return the underlying PageContext. + * Only intended for cooperating classes in this package. + */ + protected final PageContext getPageContext() { + return this.pageContext; + } + + /** + * This implementation checks for a JSTL locale attribute + * in page, request, session or application scope; if not found, + * returns the HttpServletRequest.getLocale(). + */ + protected Locale getFallbackLocale() { + if (jstlPresent) { + Locale locale = JstlPageLocaleResolver.getJstlLocale(getPageContext()); + if (locale != null) { + return locale; + } + } + return getRequest().getLocale(); + } + + + /** + * Inner class that isolates the JSTL dependency. + * Just called to resolve the fallback locale if the JSTL API is present. + */ + private static class JstlPageLocaleResolver { + + public static Locale getJstlLocale(PageContext pageContext) { + Object localeObject = Config.find(pageContext, Config.FMT_LOCALE); + return (localeObject instanceof Locale ? (Locale) localeObject : null); + } + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/support/JstlUtils.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/support/JstlUtils.java new file mode 100644 index 00000000000..5ece7eb9470 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/support/JstlUtils.java @@ -0,0 +1,144 @@ +/* + * Copyright 2002-2008 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.web.servlet.support; + +import java.util.Locale; +import java.util.ResourceBundle; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; +import javax.servlet.jsp.jstl.core.Config; +import javax.servlet.jsp.jstl.fmt.LocalizationContext; + +import org.springframework.context.MessageSource; +import org.springframework.context.support.MessageSourceResourceBundle; +import org.springframework.context.support.ResourceBundleMessageSource; + +/** + * Helper class for preparing JSTL views, + * in particular for exposing a JSTL localization context. + * + * @author Juergen Hoeller + * @since 20.08.2003 + */ +public abstract class JstlUtils { + + /** + * Checks JSTL's "javax.servlet.jsp.jstl.fmt.localizationContext" + * context-param and creates a corresponding child message source, + * with the provided Spring-defined MessageSource as parent. + * @param servletContext the ServletContext we're running in + * (to check JSTL-related context-params in web.xml) + * @param messageSource the MessageSource to expose, typically + * the ApplicationContext of the current DispatcherServlet + * @return the MessageSource to expose to JSTL; first checking the + * JSTL-defined bundle, then the Spring-defined MessageSource + * @see org.springframework.context.ApplicationContext + */ + public static MessageSource getJstlAwareMessageSource( + ServletContext servletContext, MessageSource messageSource) { + + if (servletContext != null) { + String jstlInitParam = servletContext.getInitParameter(Config.FMT_LOCALIZATION_CONTEXT); + if (jstlInitParam != null) { + // Create a ResourceBundleMessageSource for the specified resource bundle + // basename in the JSTL context-param in web.xml, wiring it with the given + // Spring-defined MessageSource as parent. + ResourceBundleMessageSource jstlBundleWrapper = new ResourceBundleMessageSource(); + jstlBundleWrapper.setBasename(jstlInitParam); + jstlBundleWrapper.setParentMessageSource(messageSource); + return jstlBundleWrapper; + } + } + return messageSource; + } + + /** + * Exposes JSTL-specific request attributes specifying locale + * and resource bundle for JSTL's formatting and message tags, + * using Spring's locale and MessageSource. + * @param request the current HTTP request + * @param messageSource the MessageSource to expose, + * typically the current ApplicationContext (may be null) + * @see #exposeLocalizationContext(RequestContext) + */ + public static void exposeLocalizationContext(HttpServletRequest request, MessageSource messageSource) { + Locale jstlLocale = RequestContextUtils.getLocale(request); + Config.set(request, Config.FMT_LOCALE, jstlLocale); + if (messageSource != null) { + LocalizationContext jstlContext = new SpringLocalizationContext(messageSource, request); + Config.set(request, Config.FMT_LOCALIZATION_CONTEXT, jstlContext); + } + } + + /** + * Exposes JSTL-specific request attributes specifying locale + * and resource bundle for JSTL's formatting and message tags, + * using Spring's locale and MessageSource. + * @param requestContext the context for the current HTTP request, + * including the ApplicationContext to expose as MessageSource + */ + public static void exposeLocalizationContext(RequestContext requestContext) { + Config.set(requestContext.getRequest(), Config.FMT_LOCALE, requestContext.getLocale()); + MessageSource messageSource = getJstlAwareMessageSource( + requestContext.getServletContext(), requestContext.getMessageSource()); + LocalizationContext jstlContext = new SpringLocalizationContext(messageSource, requestContext.getRequest()); + Config.set(requestContext.getRequest(), Config.FMT_LOCALIZATION_CONTEXT, jstlContext); + } + + + /** + * Spring-specific LocalizationContext adapter that merges session-scoped + * JSTL LocalizationContext/Locale attributes with the local Spring request context. + */ + private static class SpringLocalizationContext extends LocalizationContext { + + private final MessageSource messageSource; + + private final HttpServletRequest request; + + public SpringLocalizationContext(MessageSource messageSource, HttpServletRequest request) { + this.messageSource = messageSource; + this.request = request; + } + + public ResourceBundle getResourceBundle() { + HttpSession session = this.request.getSession(false); + if (session != null) { + Object lcObject = Config.get(session, Config.FMT_LOCALIZATION_CONTEXT); + if (lcObject instanceof LocalizationContext) { + ResourceBundle lcBundle = ((LocalizationContext) lcObject).getResourceBundle(); + return new MessageSourceResourceBundle(this.messageSource, getLocale(), lcBundle); + } + } + return new MessageSourceResourceBundle(this.messageSource, getLocale()); + } + + public Locale getLocale() { + HttpSession session = this.request.getSession(false); + if (session != null) { + Object localeObject = Config.get(session, Config.FMT_LOCALE); + if (localeObject instanceof Locale) { + return (Locale) localeObject; + } + } + return RequestContextUtils.getLocale(this.request); + } + }; + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/support/RequestContext.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/support/RequestContext.java new file mode 100644 index 00000000000..dd3857fd4d3 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/support/RequestContext.java @@ -0,0 +1,737 @@ +/* + * Copyright 2002-2008 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.web.servlet.support; + +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; +import javax.servlet.jsp.jstl.core.Config; + +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.context.NoSuchMessageException; +import org.springframework.ui.context.Theme; +import org.springframework.ui.context.ThemeSource; +import org.springframework.ui.context.support.ResourceBundleThemeSource; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.validation.BindException; +import org.springframework.validation.BindingResult; +import org.springframework.validation.Errors; +import org.springframework.web.bind.EscapedErrors; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.servlet.LocaleResolver; +import org.springframework.web.util.HtmlUtils; +import org.springframework.web.util.UrlPathHelper; +import org.springframework.web.util.WebUtils; + +/** + * Context holder for request-specific state, like current web application + * context, current locale, current theme, and potential binding errors. + * Provides easy access to localized messages and Errors instances. + * + *

Suitable for exposition to views, and usage within JSP's "useBean" tag, + * JSP scriptlets, JSTL EL, Velocity templates, etc. Necessary for views + * that do not have access to the servlet request, like Velocity templates. + * + *

Can be instantiated manually, or automatically exposed to views as + * model attribute via AbstractView's "requestContextAttribute" property. + * + *

Will also work outside of DispatcherServlet requests, accessing the root + * WebApplicationContext and using an appropriate fallback for the locale + * (the HttpServletRequest's primary locale). + * + * @author Juergen Hoeller + * @since 03.03.2003 + * @see org.springframework.web.servlet.DispatcherServlet + * @see org.springframework.web.servlet.view.AbstractView#setRequestContextAttribute + * @see org.springframework.web.servlet.view.UrlBasedViewResolver#setRequestContextAttribute + * @see #getFallbackLocale() + */ +public class RequestContext { + + /** + * Default theme name used if the RequestContext cannot find a ThemeResolver. + * Only applies to non-DispatcherServlet requests. + *

Same as AbstractThemeResolver's default, but not linked in here to + * avoid package interdependencies. + * @see org.springframework.web.servlet.theme.AbstractThemeResolver#ORIGINAL_DEFAULT_THEME_NAME + */ + public static final String DEFAULT_THEME_NAME = "theme"; + + /** + * Request attribute to hold the current web application context for RequestContext usage. + * By default, the DispatcherServlet's context (or the root context as fallback) is exposed. + */ + public static final String WEB_APPLICATION_CONTEXT_ATTRIBUTE = RequestContext.class.getName() + ".CONTEXT"; + + + protected static final boolean jstlPresent = ClassUtils.isPresent( + "javax.servlet.jsp.jstl.core.Config", JspAwareRequestContext.class.getClassLoader()); + + + private HttpServletRequest request; + + private Map model; + + private WebApplicationContext webApplicationContext; + + private Locale locale; + + private Theme theme; + + private Boolean defaultHtmlEscape; + + private UrlPathHelper urlPathHelper; + + private Map errorsMap; + + + /** + * Create a new RequestContext for the given request, + * using the request attributes for Errors retrieval. + *

This only works with InternalResourceViews, as Errors instances + * are part of the model and not normally exposed as request attributes. + * It will typically be used within JSPs or custom tags. + *

Will only work within a DispatcherServlet request. Pass in a + * ServletContext to be able to fallback to the root WebApplicationContext. + * @param request current HTTP request + * @see org.springframework.web.servlet.DispatcherServlet + * @see #RequestContext(javax.servlet.http.HttpServletRequest, javax.servlet.ServletContext) + */ + public RequestContext(HttpServletRequest request) { + initContext(request, null, null); + } + + /** + * Create a new RequestContext for the given request, + * using the request attributes for Errors retrieval. + *

This only works with InternalResourceViews, as Errors instances + * are part of the model and not normally exposed as request attributes. + * It will typically be used within JSPs or custom tags. + *

If a ServletContext is specified, the RequestContext will also + * work with the root WebApplicationContext (outside a DispatcherServlet). + * @param request current HTTP request + * @param servletContext the servlet context of the web application + * (can be null; necessary for fallback to root WebApplicationContext) + * @see org.springframework.web.context.WebApplicationContext + * @see org.springframework.web.servlet.DispatcherServlet + */ + public RequestContext(HttpServletRequest request, ServletContext servletContext) { + initContext(request, servletContext, null); + } + + /** + * Create a new RequestContext for the given request, + * using the given model attributes for Errors retrieval. + *

This works with all View implementations. + * It will typically be used by View implementations. + *

Will only work within a DispatcherServlet request. Pass in a + * ServletContext to be able to fallback to the root WebApplicationContext. + * @param request current HTTP request + * @param model the model attributes for the current view + * (can be null, using the request attributes for Errors retrieval) + * @see org.springframework.web.servlet.DispatcherServlet + * @see #RequestContext(javax.servlet.http.HttpServletRequest, javax.servlet.ServletContext, Map) + */ + public RequestContext(HttpServletRequest request, Map model) { + initContext(request, null, model); + } + + /** + * Create a new RequestContext for the given request, + * using the given model attributes for Errors retrieval. + *

This works with all View implementations. + * It will typically be used by View implementations. + *

If a ServletContext is specified, the RequestContext will also + * work with a root WebApplicationContext (outside a DispatcherServlet). + * @param request current HTTP request + * @param servletContext the servlet context of the web application + * (can be null; necessary for fallback to root WebApplicationContext) + * @param model the model attributes for the current view + * (can be null, using the request attributes for Errors retrieval) + * @see org.springframework.web.context.WebApplicationContext + * @see org.springframework.web.servlet.DispatcherServlet + */ + public RequestContext(HttpServletRequest request, ServletContext servletContext, Map model) { + initContext(request, servletContext, model); + } + + /** + * Default constructor for subclasses. + */ + protected RequestContext() { + } + + + /** + * Initialize this context with the given request, + * using the given model attributes for Errors retrieval. + *

Delegates to getFallbackLocale and getFallbackTheme + * for determining the fallback locale and theme, respectively, if no LocaleResolver + * and/or ThemeResolver can be found in the request. + * @param request current HTTP request + * @param servletContext the servlet context of the web application + * (can be null; necessary for fallback to root WebApplicationContext) + * @param model the model attributes for the current view + * (can be null, using the request attributes for Errors retrieval) + * @see #getFallbackLocale + * @see #getFallbackTheme + * @see org.springframework.web.servlet.DispatcherServlet#LOCALE_RESOLVER_ATTRIBUTE + * @see org.springframework.web.servlet.DispatcherServlet#THEME_RESOLVER_ATTRIBUTE + */ + protected void initContext(HttpServletRequest request, ServletContext servletContext, Map model) { + this.request = request; + this.model = model; + + // Fetch WebApplicationContext, either from DispatcherServlet or the root context. + // ServletContext needs to be specified to be able to fall back to the root context! + this.webApplicationContext = + (WebApplicationContext) request.getAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE); + if (this.webApplicationContext == null) { + this.webApplicationContext = RequestContextUtils.getWebApplicationContext(request, servletContext); + } + + // Determine locale to use for this RequestContext. + LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request); + if (localeResolver != null) { + // Try LocaleResolver (we're within a DispatcherServlet request). + this.locale = localeResolver.resolveLocale(request); + } + else { + // No LocaleResolver available -> try fallback. + this.locale = getFallbackLocale(); + } + + // Determine default HTML escape setting from the "defaultHtmlEscape" + // context-param in web.xml, if any. + this.defaultHtmlEscape = WebUtils.getDefaultHtmlEscape(this.webApplicationContext.getServletContext()); + + this.urlPathHelper = new UrlPathHelper(); + } + + /** + * Determine the fallback locale for this context. + *

The default implementation checks for a JSTL locale attribute + * in request, session or application scope; if not found, + * returns the HttpServletRequest.getLocale(). + * @return the fallback locale (never null) + * @see javax.servlet.http.HttpServletRequest#getLocale() + */ + protected Locale getFallbackLocale() { + if (jstlPresent) { + Locale locale = JstlLocaleResolver.getJstlLocale(getRequest(), getServletContext()); + if (locale != null) { + return locale; + } + } + return getRequest().getLocale(); + } + + /** + * Determine the fallback theme for this context. + *

The default implementation returns the default theme (with name "theme"). + * @return the fallback theme (never null) + */ + protected Theme getFallbackTheme() { + ThemeSource themeSource = RequestContextUtils.getThemeSource(getRequest()); + if (themeSource == null) { + themeSource = new ResourceBundleThemeSource(); + } + Theme theme = themeSource.getTheme(DEFAULT_THEME_NAME); + if (theme == null) { + throw new IllegalStateException("No theme defined and no fallback theme found"); + } + return theme; + } + + + /** + * Return the underlying HttpServletRequest. + * Only intended for cooperating classes in this package. + */ + protected final HttpServletRequest getRequest() { + return this.request; + } + + /** + * Return the underlying ServletContext. + * Only intended for cooperating classes in this package. + */ + protected final ServletContext getServletContext() { + return this.webApplicationContext.getServletContext(); + } + + /** + * Return the current WebApplicationContext. + */ + public final WebApplicationContext getWebApplicationContext() { + return this.webApplicationContext; + } + + /** + * Return the current WebApplicationContext as MessageSource. + */ + public final MessageSource getMessageSource() { + return this.webApplicationContext; + } + + /** + * Return the current locale. + */ + public final Locale getLocale() { + return this.locale; + } + + /** + * Return the current theme (never null). + * Resolved lazily for more efficiency when theme support is not used. + */ + public final Theme getTheme() { + if (this.theme == null) { + // Lazily determine theme to use for this RequestContext. + this.theme = RequestContextUtils.getTheme(this.request); + if (this.theme == null) { + // No ThemeResolver and ThemeSource available -> try fallback. + this.theme = getFallbackTheme(); + } + } + return this.theme; + } + + + /** + * (De)activate default HTML escaping for messages and errors, for the scope + * of this RequestContext. The default is the application-wide setting + * (the "defaultHtmlEscape" context-param in web.xml). + * @see org.springframework.web.util.WebUtils#isDefaultHtmlEscape + */ + public void setDefaultHtmlEscape(boolean defaultHtmlEscape) { + this.defaultHtmlEscape = Boolean.valueOf(defaultHtmlEscape); + } + + /** + * Is default HTML escaping active? + * Falls back to false in case of no explicit default given. + */ + public boolean isDefaultHtmlEscape() { + return (this.defaultHtmlEscape != null && this.defaultHtmlEscape.booleanValue()); + } + + /** + * Return the default HTML escape setting, differentiating + * between no default specified and an explicit value. + * @return whether default HTML escaping is enabled (null = no explicit default) + */ + public Boolean getDefaultHtmlEscape() { + return this.defaultHtmlEscape; + } + + /** + * Set the UrlPathHelper to use for context path and request URI decoding. + * Can be used to pass a shared UrlPathHelper instance in. + *

A default UrlPathHelper is always available. + */ + public void setUrlPathHelper(UrlPathHelper urlPathHelper) { + Assert.notNull(urlPathHelper, "UrlPathHelper must not be null"); + this.urlPathHelper = urlPathHelper; + } + + /** + * Return the UrlPathHelper used for context path and request URI decoding. + * Can be used to configure the current UrlPathHelper. + *

A default UrlPathHelper is always available. + */ + public UrlPathHelper getUrlPathHelper() { + return this.urlPathHelper; + } + + + /** + * Return the context path of the original request, + * that is, the path that indicates the current web application. + * This is useful for building links to other resources within the application. + *

Delegates to the UrlPathHelper for decoding. + * @see javax.servlet.http.HttpServletRequest#getContextPath + * @see #getUrlPathHelper + */ + public String getContextPath() { + return this.urlPathHelper.getOriginatingContextPath(this.request); + } + + /** + * Return the request URI of the original request, that is, the invoked URL + * without parameters. This is particularly useful as HTML form action target, + * possibly in combination with the original query string. + *

Note this implementation will correctly resolve to the URI of any + * originating root request in the presence of a forwarded request. However, this + * can only work when the Servlet 2.4 'forward' request attributes are present. + * For use in a Servlet 2.3 environment, you can rely on + * {@link org.springframework.web.servlet.view.InternalResourceView} + * to add these prior to dispatching the request. + *

Delegates to the UrlPathHelper for decoding. + * @see #getQueryString + * @see org.springframework.web.util.UrlPathHelper#getOriginatingRequestUri + * @see #getUrlPathHelper + */ + public String getRequestUri() { + return this.urlPathHelper.getOriginatingRequestUri(this.request); + } + + /** + * Return the query string of the current request, that is, the part after + * the request path. This is particularly useful for building an HTML form + * action target in combination with the original request URI. + *

Note this implementation will correctly resolve to the query string of any + * originating root request in the presence of a forwarded request. However, this + * can only work when the Servlet 2.4 'forward' request attributes are present. + * For use in a Servlet 2.3 environment, you can rely on + * {@link org.springframework.web.servlet.view.InternalResourceView} + * to add these prior to dispatching the request. + *

Delegates to the UrlPathHelper for decoding. + * @see #getRequestUri + * @see org.springframework.web.util.UrlPathHelper#getOriginatingQueryString + * @see #getUrlPathHelper + */ + public String getQueryString() { + return this.urlPathHelper.getOriginatingQueryString(this.request); + } + + + /** + * Retrieve the message for the given code, using the "defaultHtmlEscape" setting. + * @param code code of the message + * @param defaultMessage String to return if the lookup fails + * @return the message + */ + public String getMessage(String code, String defaultMessage) { + return getMessage(code, null, defaultMessage, isDefaultHtmlEscape()); + } + + /** + * Retrieve the message for the given code, using the "defaultHtmlEscape" setting. + * @param code code of the message + * @param args arguments for the message, or null if none + * @param defaultMessage String to return if the lookup fails + * @return the message + */ + public String getMessage(String code, Object[] args, String defaultMessage) { + return getMessage(code, args, defaultMessage, isDefaultHtmlEscape()); + } + + /** + * Retrieve the message for the given code, using the "defaultHtmlEscape" setting. + * @param code code of the message + * @param args arguments for the message as a List, or null if none + * @param defaultMessage String to return if the lookup fails + * @return the message + */ + public String getMessage(String code, List args, String defaultMessage) { + return getMessage(code, (args != null ? args.toArray() : null), defaultMessage, isDefaultHtmlEscape()); + } + + /** + * Retrieve the message for the given code. + * @param code code of the message + * @param args arguments for the message, or null if none + * @param defaultMessage String to return if the lookup fails + * @param htmlEscape HTML escape the message? + * @return the message + */ + public String getMessage(String code, Object[] args, String defaultMessage, boolean htmlEscape) { + String msg = this.webApplicationContext.getMessage(code, args, defaultMessage, this.locale); + return (htmlEscape ? HtmlUtils.htmlEscape(msg) : msg); + } + + /** + * Retrieve the message for the given code, using the "defaultHtmlEscape" setting. + * @param code code of the message + * @return the message + * @throws org.springframework.context.NoSuchMessageException if not found + */ + public String getMessage(String code) throws NoSuchMessageException { + return getMessage(code, null, isDefaultHtmlEscape()); + } + + /** + * Retrieve the message for the given code, using the "defaultHtmlEscape" setting. + * @param code code of the message + * @param args arguments for the message, or null if none + * @return the message + * @throws org.springframework.context.NoSuchMessageException if not found + */ + public String getMessage(String code, Object[] args) throws NoSuchMessageException { + return getMessage(code, args, isDefaultHtmlEscape()); + } + + /** + * Retrieve the message for the given code, using the "defaultHtmlEscape" setting. + * @param code code of the message + * @param args arguments for the message as a List, or null if none + * @return the message + * @throws org.springframework.context.NoSuchMessageException if not found + */ + public String getMessage(String code, List args) throws NoSuchMessageException { + return getMessage(code, (args != null ? args.toArray() : null), isDefaultHtmlEscape()); + } + + /** + * Retrieve the message for the given code. + * @param code code of the message + * @param args arguments for the message, or null if none + * @param htmlEscape HTML escape the message? + * @return the message + * @throws org.springframework.context.NoSuchMessageException if not found + */ + public String getMessage(String code, Object[] args, boolean htmlEscape) throws NoSuchMessageException { + String msg = this.webApplicationContext.getMessage(code, args, this.locale); + return (htmlEscape ? HtmlUtils.htmlEscape(msg) : msg); + } + + /** + * Retrieve the given MessageSourceResolvable (e.g. an ObjectError instance), + * using the "defaultHtmlEscape" setting. + * @param resolvable the MessageSourceResolvable + * @return the message + * @throws org.springframework.context.NoSuchMessageException if not found + */ + public String getMessage(MessageSourceResolvable resolvable) throws NoSuchMessageException { + return getMessage(resolvable, isDefaultHtmlEscape()); + } + + /** + * Retrieve the given MessageSourceResolvable (e.g. an ObjectError instance). + * @param resolvable the MessageSourceResolvable + * @param htmlEscape HTML escape the message? + * @return the message + * @throws org.springframework.context.NoSuchMessageException if not found + */ + public String getMessage(MessageSourceResolvable resolvable, boolean htmlEscape) throws NoSuchMessageException { + String msg = this.webApplicationContext.getMessage(resolvable, this.locale); + return (htmlEscape ? HtmlUtils.htmlEscape(msg) : msg); + } + + + /** + * Retrieve the theme message for the given code. + *

Note that theme messages are never HTML-escaped, as they typically + * denote theme-specific resource paths and not client-visible messages. + * @param code code of the message + * @param defaultMessage String to return if the lookup fails + * @return the message + */ + public String getThemeMessage(String code, String defaultMessage) { + return getTheme().getMessageSource().getMessage(code, null, defaultMessage, this.locale); + } + + /** + * Retrieve the theme message for the given code. + *

Note that theme messages are never HTML-escaped, as they typically + * denote theme-specific resource paths and not client-visible messages. + * @param code code of the message + * @param args arguments for the message, or null if none + * @param defaultMessage String to return if the lookup fails + * @return the message + */ + public String getThemeMessage(String code, Object[] args, String defaultMessage) { + return getTheme().getMessageSource().getMessage(code, args, defaultMessage, this.locale); + } + + /** + * Retrieve the theme message for the given code. + *

Note that theme messages are never HTML-escaped, as they typically + * denote theme-specific resource paths and not client-visible messages. + * @param code code of the message + * @param args arguments for the message as a List, or null if none + * @param defaultMessage String to return if the lookup fails + * @return the message + */ + public String getThemeMessage(String code, List args, String defaultMessage) { + return getTheme().getMessageSource().getMessage( + code, (args != null ? args.toArray() : null), defaultMessage, this.locale); + } + + /** + * Retrieve the theme message for the given code. + *

Note that theme messages are never HTML-escaped, as they typically + * denote theme-specific resource paths and not client-visible messages. + * @param code code of the message + * @return the message + * @throws org.springframework.context.NoSuchMessageException if not found + */ + public String getThemeMessage(String code) throws NoSuchMessageException { + return getTheme().getMessageSource().getMessage(code, null, this.locale); + } + + /** + * Retrieve the theme message for the given code. + *

Note that theme messages are never HTML-escaped, as they typically + * denote theme-specific resource paths and not client-visible messages. + * @param code code of the message + * @param args arguments for the message, or null if none + * @return the message + * @throws org.springframework.context.NoSuchMessageException if not found + */ + public String getThemeMessage(String code, Object[] args) throws NoSuchMessageException { + return getTheme().getMessageSource().getMessage(code, args, this.locale); + } + + /** + * Retrieve the theme message for the given code. + *

Note that theme messages are never HTML-escaped, as they typically + * denote theme-specific resource paths and not client-visible messages. + * @param code code of the message + * @param args arguments for the message as a List, or null if none + * @return the message + * @throws org.springframework.context.NoSuchMessageException if not found + */ + public String getThemeMessage(String code, List args) throws NoSuchMessageException { + return getTheme().getMessageSource().getMessage( + code, (args != null ? args.toArray() : null), this.locale); + } + + /** + * Retrieve the given MessageSourceResolvable in the current theme. + *

Note that theme messages are never HTML-escaped, as they typically + * denote theme-specific resource paths and not client-visible messages. + * @param resolvable the MessageSourceResolvable + * @return the message + * @throws org.springframework.context.NoSuchMessageException if not found + */ + public String getThemeMessage(MessageSourceResolvable resolvable) throws NoSuchMessageException { + return getTheme().getMessageSource().getMessage(resolvable, this.locale); + } + + + /** + * Retrieve the Errors instance for the given bind object, + * using the "defaultHtmlEscape" setting. + * @param name name of the bind object + * @return the Errors instance, or null if not found + */ + public Errors getErrors(String name) { + return getErrors(name, isDefaultHtmlEscape()); + } + + /** + * Retrieve the Errors instance for the given bind object. + * @param name name of the bind object + * @param htmlEscape create an Errors instance with automatic HTML escaping? + * @return the Errors instance, or null if not found + */ + public Errors getErrors(String name, boolean htmlEscape) { + if (this.errorsMap == null) { + this.errorsMap = new HashMap(); + } + Errors errors = (Errors) this.errorsMap.get(name); + boolean put = false; + if (errors == null) { + errors = (Errors) getModelObject(BindingResult.MODEL_KEY_PREFIX + name); + // Check old BindException prefix for backwards compatibility. + if (errors == null) { + errors = (Errors) getModelObject(BindException.ERROR_KEY_PREFIX + name); + } + if (errors instanceof BindException) { + errors = ((BindException) errors).getBindingResult(); + } + if (errors == null) { + return null; + } + put = true; + } + if (htmlEscape && !(errors instanceof EscapedErrors)) { + errors = new EscapedErrors(errors); + put = true; + } + else if (!htmlEscape && errors instanceof EscapedErrors) { + errors = ((EscapedErrors) errors).getSource(); + put = true; + } + if (put) { + this.errorsMap.put(name, errors); + } + return errors; + } + + /** + * Retrieve the model object for the given model name, + * either from the model or from the request attributes. + * @param modelName the name of the model object + * @return the model object + */ + protected Object getModelObject(String modelName) { + if (this.model != null) { + return this.model.get(modelName); + } + else { + return this.request.getAttribute(modelName); + } + } + + /** + * Create a BindStatus for the given bind object, + * using the "defaultHtmlEscape" setting. + * @param path the bean and property path for which values and errors + * will be resolved (e.g. "person.age") + * @return the new BindStatus instance + * @throws IllegalStateException if no corresponding Errors object found + */ + public BindStatus getBindStatus(String path) throws IllegalStateException { + return new BindStatus(this, path, isDefaultHtmlEscape()); + } + + /** + * Create a BindStatus for the given bind object, + * using the "defaultHtmlEscape" setting. + * @param path the bean and property path for which values and errors + * will be resolved (e.g. "person.age") + * @param htmlEscape create a BindStatus with automatic HTML escaping? + * @return the new BindStatus instance + * @throws IllegalStateException if no corresponding Errors object found + */ + public BindStatus getBindStatus(String path, boolean htmlEscape) throws IllegalStateException { + return new BindStatus(this, path, htmlEscape); + } + + + /** + * Inner class that isolates the JSTL dependency. + * Just called to resolve the fallback locale if the JSTL API is present. + */ + private static class JstlLocaleResolver { + + public static Locale getJstlLocale(HttpServletRequest request, ServletContext servletContext) { + Object localeObject = Config.get(request, Config.FMT_LOCALE); + if (localeObject == null) { + HttpSession session = request.getSession(false); + if (session != null) { + localeObject = Config.get(session, Config.FMT_LOCALE); + } + if (localeObject == null && servletContext != null) { + localeObject = Config.get(servletContext, Config.FMT_LOCALE); + } + } + return (localeObject instanceof Locale ? (Locale) localeObject : null); + } + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/support/RequestContextUtils.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/support/RequestContextUtils.java new file mode 100644 index 00000000000..fed2015a4a7 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/support/RequestContextUtils.java @@ -0,0 +1,159 @@ +/* + * Copyright 2002-2007 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.web.servlet.support; + +import java.util.Locale; + +import javax.servlet.ServletContext; +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; + +import org.springframework.ui.context.Theme; +import org.springframework.ui.context.ThemeSource; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.WebApplicationContextUtils; +import org.springframework.web.servlet.DispatcherServlet; +import org.springframework.web.servlet.LocaleResolver; +import org.springframework.web.servlet.ThemeResolver; + +/** + * Utility class for easy access to request-specific state which has been + * set by the {@link org.springframework.web.servlet.DispatcherServlet}. + * + *

Supports lookup of current WebApplicationContext, LocaleResolver, + * Locale, ThemeResolver, Theme, and MultipartResolver. + * + * @author Juergen Hoeller + * @since 03.03.2003 + * @see RequestContext + * @see org.springframework.web.servlet.DispatcherServlet + */ +public abstract class RequestContextUtils { + + /** + * Look for the WebApplicationContext associated with the DispatcherServlet + * that has initiated request processing. + * @param request current HTTP request + * @return the request-specific web application context + * @throws IllegalStateException if no servlet-specific context has been found + */ + public static WebApplicationContext getWebApplicationContext(ServletRequest request) + throws IllegalStateException { + + return getWebApplicationContext(request, null); + } + + /** + * Look for the WebApplicationContext associated with the DispatcherServlet + * that has initiated request processing, and for the global context if none + * was found associated with the current request. This method is useful to + * allow components outside the framework, such as JSP tag handlers, + * to access the most specific application context available. + * @param request current HTTP request + * @param servletContext current servlet context + * @return the request-specific WebApplicationContext, or the global one + * if no request-specific context has been found + * @throws IllegalStateException if neither a servlet-specific nor a + * global context has been found + */ + public static WebApplicationContext getWebApplicationContext( + ServletRequest request, ServletContext servletContext) throws IllegalStateException { + + WebApplicationContext webApplicationContext = (WebApplicationContext) request.getAttribute( + DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE); + if (webApplicationContext == null) { + if (servletContext == null) { + throw new IllegalStateException("No WebApplicationContext found: not in a DispatcherServlet request?"); + } + webApplicationContext = WebApplicationContextUtils.getWebApplicationContext(servletContext); + if (webApplicationContext == null) { + throw new IllegalStateException("No WebApplicationContext found: no ContextLoaderListener registered?"); + } + } + return webApplicationContext; + } + + /** + * Return the LocaleResolver that has been bound to the request by the + * DispatcherServlet. + * @param request current HTTP request + * @return the current LocaleResolver, or null if not found + */ + public static LocaleResolver getLocaleResolver(HttpServletRequest request) { + return (LocaleResolver) request.getAttribute(DispatcherServlet.LOCALE_RESOLVER_ATTRIBUTE); + } + + /** + * Retrieves the current locale from the given request, + * using the LocaleResolver bound to the request by the DispatcherServlet + * (if available), falling back to the request's accept-header Locale. + * @param request current HTTP request + * @return the current locale, either from the LocaleResolver or from + * the plain request + * @see #getLocaleResolver + * @see javax.servlet.http.HttpServletRequest#getLocale() + */ + public static Locale getLocale(HttpServletRequest request) { + LocaleResolver localeResolver = getLocaleResolver(request); + if (localeResolver != null) { + return localeResolver.resolveLocale(request); + } + else { + return request.getLocale(); + } + } + + /** + * Return the ThemeResolver that has been bound to the request by the + * DispatcherServlet. + * @param request current HTTP request + * @return the current ThemeResolver, or null if not found + */ + public static ThemeResolver getThemeResolver(HttpServletRequest request) { + return (ThemeResolver) request.getAttribute(DispatcherServlet.THEME_RESOLVER_ATTRIBUTE); + } + + /** + * Return the ThemeSource that has been bound to the request by the + * DispatcherServlet. + * @param request current HTTP request + * @return the current ThemeSource + */ + public static ThemeSource getThemeSource(HttpServletRequest request) { + return (ThemeSource) request.getAttribute(DispatcherServlet.THEME_SOURCE_ATTRIBUTE); + } + + /** + * Retrieves the current theme from the given request, using the ThemeResolver + * and ThemeSource bound to the request by the DispatcherServlet. + * @param request current HTTP request + * @return the current theme, or null if not found + * @see #getThemeResolver + */ + public static Theme getTheme(HttpServletRequest request) { + ThemeResolver themeResolver = getThemeResolver(request); + ThemeSource themeSource = getThemeSource(request); + if (themeResolver != null && themeSource != null) { + String themeName = themeResolver.resolveThemeName(request); + return themeSource.getTheme(themeName); + } + else { + return null; + } + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/support/WebContentGenerator.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/support/WebContentGenerator.java new file mode 100644 index 00000000000..f8f6e883adf --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/support/WebContentGenerator.java @@ -0,0 +1,351 @@ +/* + * Copyright 2002-2008 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.web.servlet.support; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.util.StringUtils; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.HttpSessionRequiredException; +import org.springframework.web.context.support.WebApplicationObjectSupport; + +/** + * Convenient superclass for any kind of web content generator, + * like {@link org.springframework.web.servlet.mvc.AbstractController} + * and {@link org.springframework.web.servlet.mvc.WebContentInterceptor}. + * Can also be used for custom handlers that have their own + * {@link org.springframework.web.servlet.HandlerAdapter}. + * + *

Supports HTTP cache control options. The usage of corresponding + * HTTP headers can be controlled via the "useExpiresHeader", + * "useCacheControlHeader" and "useCacheControlNoStore" properties. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see #setCacheSeconds + * @see #setRequireSession + */ +public abstract class WebContentGenerator extends WebApplicationObjectSupport { + + /** HTTP method "GET" */ + public static final String METHOD_GET = "GET"; + + /** HTTP method "HEAD" */ + public static final String METHOD_HEAD = "HEAD"; + + /** HTTP method "POST" */ + public static final String METHOD_POST = "POST"; + + + private static final String HEADER_PRAGMA = "Pragma"; + + private static final String HEADER_EXPIRES = "Expires"; + + private static final String HEADER_CACHE_CONTROL = "Cache-Control"; + + + /** Set of supported HTTP methods */ + private Set supportedMethods; + + private boolean requireSession = false; + + /** Use HTTP 1.0 expires header? */ + private boolean useExpiresHeader = true; + + /** Use HTTP 1.1 cache-control header? */ + private boolean useCacheControlHeader = true; + + /** Use HTTP 1.1 cache-control header value "no-store"? */ + private boolean useCacheControlNoStore = true; + + private int cacheSeconds = -1; + + + /** + * Create a new WebContentGenerator which supports + * HTTP methods GET, HEAD and POST by default. + */ + public WebContentGenerator() { + this(true); + } + + /** + * Create a new WebContentGenerator. + * @param restrictDefaultSupportedMethods true if this + * generator should support HTTP methods GET, HEAD and POST by default, + * or false if it should be unrestricted + */ + public WebContentGenerator(boolean restrictDefaultSupportedMethods) { + if (restrictDefaultSupportedMethods) { + this.supportedMethods = new HashSet(4); + this.supportedMethods.add(METHOD_GET); + this.supportedMethods.add(METHOD_HEAD); + this.supportedMethods.add(METHOD_POST); + } + } + + + /** + * Set the HTTP methods that this content generator should support. + * Default is HEAD, GET and POST. + */ + public final void setSupportedMethods(String[] methods) { + if (methods != null) { + this.supportedMethods = new HashSet(Arrays.asList(methods)); + } + else { + this.supportedMethods = null; + } + } + + /** + * Return the HTTP methods that this content generator supports. + */ + public final String[] getSupportedMethods() { + return StringUtils.toStringArray(this.supportedMethods); + } + + /** + * Set whether a session should be required to handle requests. + */ + public final void setRequireSession(boolean requireSession) { + this.requireSession = requireSession; + } + + /** + * Return whether a session is required to handle requests. + */ + public final boolean isRequireSession() { + return this.requireSession; + } + + /** + * Set whether to use the HTTP 1.0 expires header. Default is "true". + *

Note: Cache headers will only get applied if caching is enabled + * (or explicitly prevented) for the current request. + */ + public final void setUseExpiresHeader(boolean useExpiresHeader) { + this.useExpiresHeader = useExpiresHeader; + } + + /** + * Return whether the HTTP 1.0 expires header is used. + */ + public final boolean isUseExpiresHeader() { + return this.useExpiresHeader; + } + + /** + * Set whether to use the HTTP 1.1 cache-control header. Default is "true". + *

Note: Cache headers will only get applied if caching is enabled + * (or explicitly prevented) for the current request. + */ + public final void setUseCacheControlHeader(boolean useCacheControlHeader) { + this.useCacheControlHeader = useCacheControlHeader; + } + + /** + * Return whether the HTTP 1.1 cache-control header is used. + */ + public final boolean isUseCacheControlHeader() { + return this.useCacheControlHeader; + } + + /** + * Set whether to use the HTTP 1.1 cache-control header value "no-store" + * when preventing caching. Default is "true". + */ + public final void setUseCacheControlNoStore(boolean useCacheControlNoStore) { + this.useCacheControlNoStore = useCacheControlNoStore; + } + + /** + * Return whether the HTTP 1.1 cache-control header value "no-store" is used. + */ + public final boolean isUseCacheControlNoStore() { + return this.useCacheControlNoStore; + } + + /** + * Cache content for the given number of seconds. Default is -1, + * indicating no generation of cache-related headers. + *

Only if this is set to 0 (no cache) or a positive value (cache for + * this many seconds) will this class generate cache headers. + *

The headers can be overwritten by subclasses, before content is generated. + */ + public final void setCacheSeconds(int seconds) { + this.cacheSeconds = seconds; + } + + /** + * Return the number of seconds that content is cached. + */ + public final int getCacheSeconds() { + return this.cacheSeconds; + } + + + /** + * Check and prepare the given request and response according to the settings + * of this generator. Checks for supported methods and a required session, + * and applies the number of cache seconds specified for this generator. + * @param request current HTTP request + * @param response current HTTP response + * @param lastModified if the mapped handler provides Last-Modified support + * @throws ServletException if the request cannot be handled because a check failed + */ + protected final void checkAndPrepare( + HttpServletRequest request, HttpServletResponse response, boolean lastModified) + throws ServletException { + + checkAndPrepare(request, response, this.cacheSeconds, lastModified); + } + + /** + * Check and prepare the given request and response according to the settings + * of this generator. Checks for supported methods and a required session, + * and applies the given number of cache seconds. + * @param request current HTTP request + * @param response current HTTP response + * @param cacheSeconds positive number of seconds into the future that the + * response should be cacheable for, 0 to prevent caching + * @param lastModified if the mapped handler provides Last-Modified support + * @throws ServletException if the request cannot be handled because a check failed + */ + protected final void checkAndPrepare( + HttpServletRequest request, HttpServletResponse response, int cacheSeconds, boolean lastModified) + throws ServletException { + + // Check whether we should support the request method. + String method = request.getMethod(); + if (this.supportedMethods != null && !this.supportedMethods.contains(method)) { + throw new HttpRequestMethodNotSupportedException( + method, StringUtils.toStringArray(this.supportedMethods)); + } + + // Check whether a session is required. + if (this.requireSession) { + if (request.getSession(false) == null) { + throw new HttpSessionRequiredException("Pre-existing session required but none found"); + } + } + + // Do declarative cache control. + // Revalidate if the controller supports last-modified. + applyCacheSeconds(response, cacheSeconds, lastModified); + } + + /** + * Prevent the response from being cached. + * See http://www.mnot.net/cache_docs. + */ + protected final void preventCaching(HttpServletResponse response) { + response.setHeader(HEADER_PRAGMA, "no-cache"); + if (this.useExpiresHeader) { + // HTTP 1.0 header + response.setDateHeader(HEADER_EXPIRES, 1L); + } + if (this.useCacheControlHeader) { + // HTTP 1.1 header: "no-cache" is the standard value, + // "no-store" is necessary to prevent caching on FireFox. + response.setHeader(HEADER_CACHE_CONTROL, "no-cache"); + if (this.useCacheControlNoStore) { + response.addHeader(HEADER_CACHE_CONTROL, "no-store"); + } + } + } + + /** + * Set HTTP headers to allow caching for the given number of seconds. + * Does not tell the browser to revalidate the resource. + * @param response current HTTP response + * @param seconds number of seconds into the future that the response + * should be cacheable for + * @see #cacheForSeconds(javax.servlet.http.HttpServletResponse, int, boolean) + */ + protected final void cacheForSeconds(HttpServletResponse response, int seconds) { + cacheForSeconds(response, seconds, false); + } + + /** + * Set HTTP headers to allow caching for the given number of seconds. + * Tells the browser to revalidate the resource if mustRevalidate is + * true. + * @param response the current HTTP response + * @param seconds number of seconds into the future that the response + * should be cacheable for + * @param mustRevalidate whether the client should revalidate the resource + * (typically only necessary for controllers with last-modified support) + */ + protected final void cacheForSeconds(HttpServletResponse response, int seconds, boolean mustRevalidate) { + if (this.useExpiresHeader) { + // HTTP 1.0 header + response.setDateHeader(HEADER_EXPIRES, System.currentTimeMillis() + seconds * 1000L); + } + if (this.useCacheControlHeader) { + // HTTP 1.1 header + String headerValue = "max-age=" + seconds; + if (mustRevalidate) { + headerValue += ", must-revalidate"; + } + response.setHeader(HEADER_CACHE_CONTROL, headerValue); + } + } + + /** + * Apply the given cache seconds and generate corresponding HTTP headers, + * i.e. allow caching for the given number of seconds in case of a positive + * value, prevent caching if given a 0 value, do nothing else. + * Does not tell the browser to revalidate the resource. + * @param response current HTTP response + * @param seconds positive number of seconds into the future that the + * response should be cacheable for, 0 to prevent caching + * @see #cacheForSeconds(javax.servlet.http.HttpServletResponse, int, boolean) + */ + protected final void applyCacheSeconds(HttpServletResponse response, int seconds) { + applyCacheSeconds(response, seconds, false); + } + + /** + * Apply the given cache seconds and generate respective HTTP headers. + *

That is, allow caching for the given number of seconds in the + * case of a positive value, prevent caching if given a 0 value, else + * do nothing (i.e. leave caching to the client). + * @param response the current HTTP response + * @param seconds the (positive) number of seconds into the future that + * the response should be cacheable for; 0 to prevent caching; and + * a negative value to leave caching to the client. + * @param mustRevalidate whether the client should revalidate the resource + * (typically only necessary for controllers with last-modified support) + */ + protected final void applyCacheSeconds(HttpServletResponse response, int seconds, boolean mustRevalidate) { + if (seconds > 0) { + cacheForSeconds(response, seconds, mustRevalidate); + } + else if (seconds == 0) { + preventCaching(response); + } + // Leave caching to the client otherwise. + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/support/package.html b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/support/package.html new file mode 100644 index 00000000000..b9a07a10c6d --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/support/package.html @@ -0,0 +1,9 @@ + + + +Support classes for Spring's web MVC framework. +Provides easy evaluation of the request context in views, +and miscellaneous HandlerInterceptor implementations. + + + diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/BindErrorsTag.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/BindErrorsTag.java new file mode 100644 index 00000000000..062fb873bbe --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/BindErrorsTag.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2008 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.web.servlet.tags; + +import javax.servlet.ServletException; +import javax.servlet.jsp.JspException; +import javax.servlet.jsp.PageContext; + +import org.springframework.validation.Errors; +import org.springframework.web.util.ExpressionEvaluationUtils; + +/** + * JSP tag that evaluates content if there are binding errors + * for a certain bean. Exports an "errors" variable of type + * {@link org.springframework.validation.Errors} for the given bean. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see BindTag + * @see org.springframework.validation.Errors + */ +public class BindErrorsTag extends HtmlEscapingAwareTag { + + public static final String ERRORS_VARIABLE_NAME = "errors"; + + + private String name; + + private Errors errors; + + + /** + * Set the name of the bean that this tag should check. + */ + public void setName(String name) { + this.name = name; + } + + /** + * Return the name of the bean that this tag checks. + */ + public String getName() { + return this.name; + } + + + protected final int doStartTagInternal() throws ServletException, JspException { + String resolvedName = ExpressionEvaluationUtils.evaluateString("name", this.name, pageContext); + this.errors = getRequestContext().getErrors(resolvedName, isHtmlEscape()); + + if (this.errors != null && this.errors.hasErrors()) { + this.pageContext.setAttribute(ERRORS_VARIABLE_NAME, this.errors, PageContext.REQUEST_SCOPE); + return EVAL_BODY_INCLUDE; + } + else { + return SKIP_BODY; + } + } + + public int doEndTag() { + this.pageContext.removeAttribute(ERRORS_VARIABLE_NAME, PageContext.REQUEST_SCOPE); + return EVAL_PAGE; + } + + /** + * Retrieve the Errors instance that this tag is currently bound to. + *

Intended for cooperating nesting tags. + */ + public final Errors getErrors() { + return this.errors; + } + + + public void doFinally() { + super.doFinally(); + this.errors = null; + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/BindTag.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/BindTag.java new file mode 100644 index 00000000000..ce0dcf3d72b --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/BindTag.java @@ -0,0 +1,183 @@ +/* + * Copyright 2002-2007 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.web.servlet.tags; + +import java.beans.PropertyEditor; + +import javax.servlet.jsp.JspTagException; +import javax.servlet.jsp.PageContext; + +import org.springframework.validation.Errors; +import org.springframework.web.servlet.support.BindStatus; +import org.springframework.web.util.ExpressionEvaluationUtils; + +/** + * Bind tag, supporting evaluation of binding errors for a certain + * bean or bean property. Exposes a "status" variable of type + * {@link org.springframework.web.servlet.support.BindStatus}, + * to both Java expressions and JSP EL expressions. + * + *

Can be used to bind to any bean or bean property in the model. + * The specified path determines whether the tag exposes the status of the + * bean itself (showing object-level errors), a specific bean property + * (showing field errors), or a matching set of bean properties + * (showing all corresponding field errors). + * + *

The {@link org.springframework.validation.Errors} object that has + * been bound using this tag is exposed to collaborating tags, as well + * as the bean property that this errors object applies to. Nested tags + * such as the {@link TransformTag} can access those exposed properties. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see #setPath + */ +public class BindTag extends HtmlEscapingAwareTag implements EditorAwareTag { + + /** + * Name of the exposed variable within the scope of this tag: "status". + */ + public static final String STATUS_VARIABLE_NAME = "status"; + + + private String path; + + private boolean ignoreNestedPath = false; + + private BindStatus status; + + private Object previousPageStatus; + + private Object previousRequestStatus; + + + /** + * Set the path that this tag should apply. Can be a bean (e.g. "person") + * to get global errors, or a bean property (e.g. "person.name") to get + * field errors (also supporting nested fields and "person.na*" mappings). + * "person.*" will return all errors for the specified bean, both global + * and field errors. + * @see org.springframework.validation.Errors#getGlobalErrors + * @see org.springframework.validation.Errors#getFieldErrors + */ + public void setPath(String path) { + this.path = path; + } + + /** + * Return the path that this tag applies to. + */ + public String getPath() { + return this.path; + } + + /** + * Set whether to ignore a nested path, if any. + * Default is to not ignore. + */ + public void setIgnoreNestedPath(boolean ignoreNestedPath) { + this.ignoreNestedPath = ignoreNestedPath; + } + + /** + * Return whether to ignore a nested path, if any. + */ + public boolean isIgnoreNestedPath() { + return this.ignoreNestedPath; + } + + + protected final int doStartTagInternal() throws Exception { + String resolvedPath = ExpressionEvaluationUtils.evaluateString("path", getPath(), pageContext); + + if (!isIgnoreNestedPath()) { + String nestedPath = (String) pageContext.getAttribute( + NestedPathTag.NESTED_PATH_VARIABLE_NAME, PageContext.REQUEST_SCOPE); + // only prepend if not already an absolute path + if (nestedPath != null && !resolvedPath.startsWith(nestedPath) && + !resolvedPath.equals(nestedPath.substring(0, nestedPath.length() - 1))) { + resolvedPath = nestedPath + resolvedPath; + } + } + + try { + this.status = new BindStatus(getRequestContext(), resolvedPath, isHtmlEscape()); + } + catch (IllegalStateException ex) { + throw new JspTagException(ex.getMessage()); + } + + // Save previous status values, for re-exposure at the end of this tag. + this.previousPageStatus = pageContext.getAttribute(STATUS_VARIABLE_NAME, PageContext.PAGE_SCOPE); + this.previousRequestStatus = pageContext.getAttribute(STATUS_VARIABLE_NAME, PageContext.REQUEST_SCOPE); + + // Expose this tag's status object as PageContext attribute, + // making it available for JSP EL. + pageContext.removeAttribute(STATUS_VARIABLE_NAME, PageContext.PAGE_SCOPE); + pageContext.setAttribute(STATUS_VARIABLE_NAME, this.status, PageContext.REQUEST_SCOPE); + + return EVAL_BODY_INCLUDE; + } + + public int doEndTag() { + // Reset previous status values. + if (this.previousPageStatus != null) { + pageContext.setAttribute(STATUS_VARIABLE_NAME, this.previousPageStatus, PageContext.PAGE_SCOPE); + } + if (this.previousRequestStatus != null) { + pageContext.setAttribute(STATUS_VARIABLE_NAME, this.previousRequestStatus, PageContext.REQUEST_SCOPE); + } + else { + pageContext.removeAttribute(STATUS_VARIABLE_NAME, PageContext.REQUEST_SCOPE); + } + return EVAL_PAGE; + } + + + /** + * Retrieve the property that this tag is currently bound to, + * or null if bound to an object rather than a specific property. + * Intended for cooperating nesting tags. + * @return the property that this tag is currently bound to, + * or null if none + */ + public final String getProperty() { + return this.status.getExpression(); + } + + /** + * Retrieve the Errors instance that this tag is currently bound to. + * Intended for cooperating nesting tags. + * @return the current Errors instance, or null if none + */ + public final Errors getErrors() { + return this.status.getErrors(); + } + + public final PropertyEditor getEditor() { + return this.status.getEditor(); + } + + + public void doFinally() { + super.doFinally(); + this.status = null; + this.previousPageStatus = null; + this.previousRequestStatus = null; + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/EditorAwareTag.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/EditorAwareTag.java new file mode 100644 index 00000000000..60be14261b4 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/EditorAwareTag.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2007 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.web.servlet.tags; + +import java.beans.PropertyEditor; + +import javax.servlet.jsp.JspException; + +/** + * Interface to be implemented by JSP tags that expose a + * PropertyEditor for a property that they are currently bound to. + * + * @author Juergen Hoeller + * @since 2.5 + * @see BindTag + * @see org.springframework.web.servlet.tags.form.AbstractDataBoundFormElementTag + */ +public interface EditorAwareTag { + + /** + * Retrieve the PropertyEditor for the property that this tag is + * currently bound to. Intended for cooperating nesting tags. + * @return the current PropertyEditor, or null if none + * @throws JspException if resolving the editor failed + */ + PropertyEditor getEditor() throws JspException; + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/EscapeBodyTag.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/EscapeBodyTag.java new file mode 100644 index 00000000000..9993ba0c175 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/EscapeBodyTag.java @@ -0,0 +1,109 @@ +/* + * Copyright 2002-2005 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.web.servlet.tags; + +import java.io.IOException; + +import javax.servlet.jsp.JspException; +import javax.servlet.jsp.tagext.BodyContent; +import javax.servlet.jsp.tagext.BodyTag; + +import org.springframework.web.util.ExpressionEvaluationUtils; +import org.springframework.web.util.HtmlUtils; +import org.springframework.web.util.JavaScriptUtils; + +/** + * Custom JSP tag to escape its enclosed body content, + * applying HTML escaping and/or JavaScript escaping. + * + *

Provides a "htmlEscape" property for explicitly specifying whether to + * apply HTML escaping. If not set, a page-level default (e.g. from the + * HtmlEscapeTag) or an application-wide default (the "defaultHtmlEscape" + * context-param in web.xml) is used. + * + *

Provides a "javaScriptEscape" property for specifying whether to apply + * JavaScript escaping. Can be combined with HTML escaping or used standalone. + * + * @author Juergen Hoeller + * @since 1.1.1 + * @see org.springframework.web.util.HtmlUtils + * @see org.springframework.web.util.JavaScriptUtils + */ +public class EscapeBodyTag extends HtmlEscapingAwareTag implements BodyTag { + + private boolean javaScriptEscape = false; + + private BodyContent bodyContent; + + + /** + * Set JavaScript escaping for this tag, as boolean value. + * Default is "false". + */ + public void setJavaScriptEscape(String javaScriptEscape) throws JspException { + this.javaScriptEscape = + ExpressionEvaluationUtils.evaluateBoolean("javaScriptEscape", javaScriptEscape, pageContext); + } + + + protected int doStartTagInternal() { + // do nothing + return EVAL_BODY_BUFFERED; + } + + public void doInitBody() { + // do nothing + } + + public void setBodyContent(BodyContent bodyContent) { + this.bodyContent = bodyContent; + } + + public int doAfterBody() throws JspException { + try { + String content = readBodyContent(); + // HTML and/or JavaScript escape, if demanded + content = isHtmlEscape() ? HtmlUtils.htmlEscape(content) : content; + content = this.javaScriptEscape ? JavaScriptUtils.javaScriptEscape(content) : content; + writeBodyContent(content); + } + catch (IOException ex) { + throw new JspException("Could not write escaped body", ex); + } + return (SKIP_BODY); + } + + /** + * Read the unescaped body content from the page. + * @return the original content + * @throws IOException if reading failed + */ + protected String readBodyContent() throws IOException { + return this.bodyContent.getString(); + } + + /** + * Write the escaped body content to the page. + *

Can be overridden in subclasses, e.g. for testing purposes. + * @param content the content to write + * @throws IOException if writing failed + */ + protected void writeBodyContent(String content) throws IOException { + this.bodyContent.getEnclosingWriter().print(content); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/HtmlEscapeTag.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/HtmlEscapeTag.java new file mode 100644 index 00000000000..d036dbd798e --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/HtmlEscapeTag.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2005 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.web.servlet.tags; + +import javax.servlet.jsp.JspException; + +import org.springframework.web.util.ExpressionEvaluationUtils; + +/** + * Sets default HTML escape value for the current page. The actual value + * can be overridden by escaping-aware tags. The default is "false". + * + *

Note: You can also set a "defaultHtmlEscape" web.xml context-param. + * A page-level setting overrides a context-param. + * + * @author Juergen Hoeller + * @since 04.03.2003 + * @see HtmlEscapingAwareTag#setHtmlEscape + */ +public class HtmlEscapeTag extends RequestContextAwareTag { + + private String defaultHtmlEscape; + + + /** + * Set the default value for HTML escaping, + * to be put into the current PageContext. + */ + public void setDefaultHtmlEscape(String defaultHtmlEscape) { + this.defaultHtmlEscape = defaultHtmlEscape; + } + + + protected int doStartTagInternal() throws JspException { + boolean resolvedDefaultHtmlEscape = + ExpressionEvaluationUtils.evaluateBoolean("defaultHtmlEscape", this.defaultHtmlEscape, pageContext); + getRequestContext().setDefaultHtmlEscape(resolvedDefaultHtmlEscape); + return EVAL_BODY_INCLUDE; + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/HtmlEscapingAwareTag.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/HtmlEscapingAwareTag.java new file mode 100644 index 00000000000..571ff4c566c --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/HtmlEscapingAwareTag.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2007 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.web.servlet.tags; + +import javax.servlet.jsp.JspException; + +import org.springframework.web.util.ExpressionEvaluationUtils; + +/** + * Superclass for tags that output content that might get HTML-escaped. + * + *

Provides a "htmlEscape" property for explicitly specifying whether to + * apply HTML escaping. If not set, a page-level default (e.g. from the + * HtmlEscapeTag) or an application-wide default (the "defaultHtmlEscape" + * context-param in web.xml) is used. + * + * @author Juergen Hoeller + * @since 1.1 + * @see #setHtmlEscape + * @see HtmlEscapeTag + * @see org.springframework.web.servlet.support.RequestContext#isDefaultHtmlEscape + * @see org.springframework.web.util.WebUtils#isDefaultHtmlEscape + */ +public abstract class HtmlEscapingAwareTag extends RequestContextAwareTag { + + private Boolean htmlEscape; + + + /** + * Set HTML escaping for this tag, as boolean value. + * Overrides the default HTML escaping setting for the current page. + * @see HtmlEscapeTag#setDefaultHtmlEscape + */ + public void setHtmlEscape(String htmlEscape) throws JspException { + this.htmlEscape = + new Boolean(ExpressionEvaluationUtils.evaluateBoolean("htmlEscape", htmlEscape, pageContext)); + } + + /** + * Return the HTML escaping setting for this tag, + * or the default setting if not overridden. + * @see #isDefaultHtmlEscape() + */ + protected boolean isHtmlEscape() { + if (this.htmlEscape != null) { + return this.htmlEscape.booleanValue(); + } + else { + return isDefaultHtmlEscape(); + } + } + + /** + * Return the applicable default HTML escape setting for this tag. + *

The default implementation checks the RequestContext's setting, + * falling back to false in case of no explicit default given. + * @see #getRequestContext() + */ + protected boolean isDefaultHtmlEscape() { + return getRequestContext().isDefaultHtmlEscape(); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/MessageTag.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/MessageTag.java new file mode 100644 index 00000000000..5521bcd9e45 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/MessageTag.java @@ -0,0 +1,303 @@ +/* + * Copyright 2002-2008 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.web.servlet.tags; + +import java.io.IOException; +import java.util.Collection; + +import javax.servlet.jsp.JspException; +import javax.servlet.jsp.JspTagException; + +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.context.NoSuchMessageException; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.util.ExpressionEvaluationUtils; +import org.springframework.web.util.HtmlUtils; +import org.springframework.web.util.JavaScriptUtils; +import org.springframework.web.util.TagUtils; + +/** + * Custom JSP tag to look up a message in the scope of this page. Messages are + * resolved using the ApplicationContext and thus support internationalization. + * + *

Detects an HTML escaping setting, either on this tag instance, the page level, + * or the web.xml level. Can also apply JavaScript escaping. + * + *

If "code" isn't set or cannot be resolved, "text" will be used as default + * message. Thus, this tag can also be used for HTML escaping of any texts. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see #setCode + * @see #setText + * @see #setHtmlEscape + * @see #setJavaScriptEscape + * @see HtmlEscapeTag#setDefaultHtmlEscape + * @see org.springframework.web.util.WebUtils#HTML_ESCAPE_CONTEXT_PARAM + */ +public class MessageTag extends HtmlEscapingAwareTag { + + /** + * Default separator for splitting an arguments String: a comma (",") + */ + public static final String DEFAULT_ARGUMENT_SEPARATOR = ","; + + + private Object message; + + private String code; + + private Object arguments; + + private String argumentSeparator = DEFAULT_ARGUMENT_SEPARATOR; + + private String text; + + private String var; + + private String scope = TagUtils.SCOPE_PAGE; + + private boolean javaScriptEscape = false; + + + /** + * Set the MessageSourceResolvable for this tag. + * Accepts a direct MessageSourceResolvable instance as well as a JSP + * expression language String that points to a MessageSourceResolvable. + *

If a MessageSourceResolvable is specified, it effectively overrides + * any code, arguments or text specified on this tag. + */ + public void setMessage(Object message) { + this.message = message; + } + + /** + * Set the message code for this tag. + */ + public void setCode(String code) { + this.code = code; + } + + /** + * Set optional message arguments for this tag, as a comma-delimited + * String (each String argument can contain JSP EL), an Object array + * (used as argument array), or a single Object (used as single argument). + */ + public void setArguments(Object arguments) { + this.arguments = arguments; + } + + /** + * Set the separator to use for splitting an arguments String. + * Default is a comma (","). + * @see #setArguments + */ + public void setArgumentSeparator(String argumentSeparator) { + this.argumentSeparator = argumentSeparator; + } + + /** + * Set the message text for this tag. + */ + public void setText(String text) { + this.text = text; + } + + /** + * Set PageContext attribute name under which to expose + * a variable that contains the resolved message. + * @see #setScope + * @see javax.servlet.jsp.PageContext#setAttribute + */ + public void setVar(String var) { + this.var = var; + } + + /** + * Set the scope to export the variable to. + * Default is SCOPE_PAGE ("page"). + * @see #setVar + * @see org.springframework.web.util.TagUtils#SCOPE_PAGE + * @see javax.servlet.jsp.PageContext#setAttribute + */ + public void setScope(String scope) { + this.scope = scope; + } + + /** + * Set JavaScript escaping for this tag, as boolean value. + * Default is "false". + */ + public void setJavaScriptEscape(String javaScriptEscape) throws JspException { + this.javaScriptEscape = + ExpressionEvaluationUtils.evaluateBoolean("javaScriptEscape", javaScriptEscape, pageContext); + } + + + /** + * Resolves the message, escapes it if demanded, + * and writes it to the page (or exposes it as variable). + * @see #resolveMessage() + * @see org.springframework.web.util.HtmlUtils#htmlEscape(String) + * @see org.springframework.web.util.JavaScriptUtils#javaScriptEscape(String) + * @see #writeMessage(String) + */ + protected final int doStartTagInternal() throws JspException, IOException { + try { + // Resolve the unescaped message. + String msg = resolveMessage(); + + // HTML and/or JavaScript escape, if demanded. + msg = isHtmlEscape() ? HtmlUtils.htmlEscape(msg) : msg; + msg = this.javaScriptEscape ? JavaScriptUtils.javaScriptEscape(msg) : msg; + + // Expose as variable, if demanded, else write to the page. + String resolvedVar = ExpressionEvaluationUtils.evaluateString("var", this.var, pageContext); + if (resolvedVar != null) { + String resolvedScope = ExpressionEvaluationUtils.evaluateString("scope", this.scope, pageContext); + pageContext.setAttribute(resolvedVar, msg, TagUtils.getScope(resolvedScope)); + } + else { + writeMessage(msg); + } + + return EVAL_BODY_INCLUDE; + } + catch (NoSuchMessageException ex) { + throw new JspTagException(getNoSuchMessageExceptionDescription(ex)); + } + } + + /** + * Resolve the specified message into a concrete message String. + * The returned message String should be unescaped. + */ + protected String resolveMessage() throws JspException, NoSuchMessageException { + MessageSource messageSource = getMessageSource(); + if (messageSource == null) { + throw new JspTagException("No corresponding MessageSource found"); + } + + // Evaluate the specified MessageSourceResolvable, if any. + MessageSourceResolvable resolvedMessage = null; + if (this.message instanceof MessageSourceResolvable) { + resolvedMessage = (MessageSourceResolvable) this.message; + } + else if (this.message != null) { + String expr = this.message.toString(); + resolvedMessage = (MessageSourceResolvable) + ExpressionEvaluationUtils.evaluate("message", expr, MessageSourceResolvable.class, pageContext); + } + + if (resolvedMessage != null) { + // We have a given MessageSourceResolvable. + return messageSource.getMessage(resolvedMessage, getRequestContext().getLocale()); + } + + String resolvedCode = ExpressionEvaluationUtils.evaluateString("code", this.code, pageContext); + String resolvedText = ExpressionEvaluationUtils.evaluateString("text", this.text, pageContext); + + if (resolvedCode != null || resolvedText != null) { + // We have a code or default text that we need to resolve. + Object[] argumentsArray = resolveArguments(this.arguments); + if (resolvedText != null) { + // We have a fallback text to consider. + return messageSource.getMessage( + resolvedCode, argumentsArray, resolvedText, getRequestContext().getLocale()); + } + else { + // We have no fallback text to consider. + return messageSource.getMessage( + resolvedCode, argumentsArray, getRequestContext().getLocale()); + } + } + + // All we have is a specified literal text. + return resolvedText; + } + + /** + * Resolve the given arguments Object into an arguments array. + * @param arguments the specified arguments Object + * @return the resolved arguments as array + * @throws JspException if argument conversion failed + * @see #setArguments + */ + protected Object[] resolveArguments(Object arguments) throws JspException { + if (arguments instanceof String) { + String[] stringArray = + StringUtils.delimitedListToStringArray((String) arguments, this.argumentSeparator); + if (stringArray.length == 1) { + Object argument = ExpressionEvaluationUtils.evaluate("argument", stringArray[0], pageContext); + if (argument != null && argument.getClass().isArray()) { + return ObjectUtils.toObjectArray(argument); + } + else { + return new Object[] {argument}; + } + } + else { + Object[] argumentsArray = new Object[stringArray.length]; + for (int i = 0; i < stringArray.length; i++) { + argumentsArray[i] = + ExpressionEvaluationUtils.evaluate("argument[" + i + "]", stringArray[i], pageContext); + } + return argumentsArray; + } + } + else if (arguments instanceof Object[]) { + return (Object[]) arguments; + } + else if (arguments instanceof Collection) { + return ((Collection) arguments).toArray(); + } + else if (arguments != null) { + // Assume a single argument object. + return new Object[] {arguments}; + } + else { + return null; + } + } + + /** + * Write the message to the page. + *

Can be overridden in subclasses, e.g. for testing purposes. + * @param msg the message to write + * @throws IOException if writing failed + */ + protected void writeMessage(String msg) throws IOException { + pageContext.getOut().write(String.valueOf(msg)); + } + + /** + * Use the current RequestContext's application context as MessageSource. + */ + protected MessageSource getMessageSource() { + return getRequestContext().getMessageSource(); + } + + /** + * Return default exception message. + */ + protected String getNoSuchMessageExceptionDescription(NoSuchMessageException ex) { + return ex.getMessage(); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/NestedPathTag.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/NestedPathTag.java new file mode 100644 index 00000000000..e6741645711 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/NestedPathTag.java @@ -0,0 +1,119 @@ +/* + * Copyright 2002-2007 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.web.servlet.tags; + +import javax.servlet.jsp.JspException; +import javax.servlet.jsp.PageContext; +import javax.servlet.jsp.tagext.TagSupport; +import javax.servlet.jsp.tagext.TryCatchFinally; + +import org.springframework.beans.PropertyAccessor; +import org.springframework.web.util.ExpressionEvaluationUtils; + +/** + *

Nested-path tag, to support and assist with nested beans or bean properties + * in the model. Exports a "nestedPath" variable of type String in request scope, + * visible to the current page and also included pages, if any. + * + *

The BindTag will auto-detect the current nested path and automatically + * prepend it to its own path to form a complete path to the bean or bean property. + * + *

This tag will also prepend any existing nested path that is currently set. + * Thus, you can nest multiple nested-path tags. + * + *

Thanks to Seth Ladd for the suggestion and the original implementation! + * + * @author Juergen Hoeller + * @since 1.1 + */ +public class NestedPathTag extends TagSupport implements TryCatchFinally { + + /** + * Name of the exposed variable within the scope of this tag: "nestedPath". + */ + public static final String NESTED_PATH_VARIABLE_NAME = "nestedPath"; + + + private String path; + + /** Caching a previous nested path, so that it may be reset */ + private String previousNestedPath; + + + /** + * Set the path that this tag should apply. + *

E.g. "customer" to allow bind paths like "address.street" + * rather than "customer.address.street". + * @see BindTag#setPath + */ + public void setPath(String path) { + if (path == null) { + path = ""; + } + if (path.length() > 0 && !path.endsWith(PropertyAccessor.NESTED_PROPERTY_SEPARATOR)) { + path += PropertyAccessor.NESTED_PROPERTY_SEPARATOR; + } + this.path = path; + } + + /** + * Return the path that this tag applies to. + */ + public String getPath() { + return this.path; + } + + + public int doStartTag() throws JspException { + String resolvedPath = ExpressionEvaluationUtils.evaluateString("path", getPath(), pageContext); + + // Save previous nestedPath value, build and expose current nestedPath value. + // Use request scope to expose nestedPath to included pages too. + this.previousNestedPath = + (String) pageContext.getAttribute(NESTED_PATH_VARIABLE_NAME, PageContext.REQUEST_SCOPE); + String nestedPath = + (this.previousNestedPath != null ? this.previousNestedPath + resolvedPath : resolvedPath); + pageContext.setAttribute(NESTED_PATH_VARIABLE_NAME, nestedPath, PageContext.REQUEST_SCOPE); + + return EVAL_BODY_INCLUDE; + } + + /** + * Reset any previous nestedPath value. + */ + public int doEndTag() { + if (this.previousNestedPath != null) { + // Expose previous nestedPath value. + pageContext.setAttribute(NESTED_PATH_VARIABLE_NAME, this.previousNestedPath, PageContext.REQUEST_SCOPE); + } + else { + // Remove exposed nestedPath value. + pageContext.removeAttribute(NESTED_PATH_VARIABLE_NAME, PageContext.REQUEST_SCOPE); + } + + return EVAL_PAGE; + } + + public void doCatch(Throwable throwable) throws Throwable { + throw throwable; + } + + public void doFinally() { + this.previousNestedPath = null; + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/RequestContextAwareTag.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/RequestContextAwareTag.java new file mode 100644 index 00000000000..79d1428f2fd --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/RequestContextAwareTag.java @@ -0,0 +1,118 @@ +/* + * Copyright 2002-2006 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.web.servlet.tags; + +import javax.servlet.jsp.JspException; +import javax.servlet.jsp.JspTagException; +import javax.servlet.jsp.tagext.TagSupport; +import javax.servlet.jsp.tagext.TryCatchFinally; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.web.servlet.support.JspAwareRequestContext; +import org.springframework.web.servlet.support.RequestContext; + +/** + * Superclass for all tags that require a {@link RequestContext}. + * + *

The RequestContext instance provides easy access + * to current state like the + * {@link org.springframework.web.context.WebApplicationContext}, + * the {@link java.util.Locale}, the + * {@link org.springframework.ui.context.Theme}, etc. + * + *

Mainly intended for + * {@link org.springframework.web.servlet.DispatcherServlet} requests; + * will use fallbacks when used outside DispatcherServlet. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see org.springframework.web.servlet.support.RequestContext + * @see org.springframework.web.servlet.DispatcherServlet + */ +public abstract class RequestContextAwareTag extends TagSupport implements TryCatchFinally { + + /** {@link javax.servlet.jsp.PageContext} attribute for page-level + * {@link RequestContext} instance. + * */ + public static final String REQUEST_CONTEXT_PAGE_ATTRIBUTE = + "org.springframework.web.servlet.tags.REQUEST_CONTEXT"; + + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + + private RequestContext requestContext; + + + /** + * Create and expose the current RequestContext. + * Delegates to {@link #doStartTagInternal()} for actual work. + * @see #REQUEST_CONTEXT_PAGE_ATTRIBUTE + * @see org.springframework.web.servlet.support.JspAwareRequestContext + */ + public final int doStartTag() throws JspException { + this.requestContext = (RequestContext) this.pageContext.getAttribute(REQUEST_CONTEXT_PAGE_ATTRIBUTE); + try { + if (this.requestContext == null) { + this.requestContext = new JspAwareRequestContext(this.pageContext); + this.pageContext.setAttribute(REQUEST_CONTEXT_PAGE_ATTRIBUTE, this.requestContext); + } + return doStartTagInternal(); + } + catch (JspException ex) { + logger.error(ex.getMessage(), ex); + throw ex; + } + catch (RuntimeException ex) { + logger.error(ex.getMessage(), ex); + throw ex; + } + catch (Exception ex) { + logger.error(ex.getMessage(), ex); + throw new JspTagException(ex.getMessage()); + } + } + + /** + * Return the current RequestContext. + */ + protected final RequestContext getRequestContext() { + return this.requestContext; + } + + /** + * Called by doStartTag to perform the actual work. + * @return same as TagSupport.doStartTag + * @throws Exception any exception, any checked one other than + * a JspException gets wrapped in a JspException by doStartTag + * @see javax.servlet.jsp.tagext.TagSupport#doStartTag + */ + protected abstract int doStartTagInternal() throws Exception; + + + public void doCatch(Throwable throwable) throws Throwable { + throw throwable; + } + + public void doFinally() { + this.requestContext = null; + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/ThemeTag.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/ThemeTag.java new file mode 100644 index 00000000000..1992f92c667 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/ThemeTag.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2008 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.web.servlet.tags; + +import org.springframework.context.MessageSource; +import org.springframework.context.NoSuchMessageException; + +/** + * Custom tag to look up a theme message in the scope of this page. + * Messages are looked up using the ApplicationContext's ThemeSource, + * and thus should support internationalization. + * + *

Regards a HTML escaping setting, either on this tag instance, + * the page level, or the web.xml level. + * + *

If "code" isn't set or cannot be resolved, "text" will be used + * as default message. + * + * @author Jean-Pierre Pawlak + * @author Juergen Hoeller + * @see org.springframework.ui.context.Theme + * @see org.springframework.ui.context.ThemeSource + * @see #setCode + * @see #setText + * @see #setHtmlEscape + * @see HtmlEscapeTag#setDefaultHtmlEscape + * @see org.springframework.web.util.WebUtils#HTML_ESCAPE_CONTEXT_PARAM + */ +public class ThemeTag extends MessageTag { + + /** + * Use the theme MessageSource for theme message resolution. + */ + protected MessageSource getMessageSource() { + return getRequestContext().getTheme().getMessageSource(); + } + + /** + * Return exception message that indicates the current theme. + */ + protected String getNoSuchMessageExceptionDescription(NoSuchMessageException ex) { + return "Theme '" + getRequestContext().getTheme().getName() + "': " + ex.getMessage(); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/TransformTag.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/TransformTag.java new file mode 100644 index 00000000000..b4ac8f8c77e --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/TransformTag.java @@ -0,0 +1,139 @@ +/* + * Copyright 2002-2007 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.web.servlet.tags; + +import java.beans.PropertyEditor; +import java.io.IOException; + +import javax.servlet.jsp.JspException; +import javax.servlet.jsp.tagext.TagSupport; + +import org.springframework.web.util.ExpressionEvaluationUtils; +import org.springframework.web.util.HtmlUtils; +import org.springframework.web.util.TagUtils; + +/** + * Tag for transforming reference data values from form controllers and + * other objects inside a spring:bind tag (or a data-bound + * form element tag from Spring's form tag library). + * + *

The BindTag has a PropertyEditor that it uses to transform properties of + * a bean to a String, useable in HTML forms. This tag uses that PropertyEditor + * to transform objects passed into this tag. + * + * @author Alef Arendsen + * @author Juergen Hoeller + * @since 20.09.2003 + * @see BindTag + */ +public class TransformTag extends HtmlEscapingAwareTag { + + /** the value to transform using the appropriate property editor */ + private Object value; + + /** the variable to put the result in */ + private String var; + + /** the scope of the variable the result will be put in */ + private String scope = TagUtils.SCOPE_PAGE; + + + /** + * Set the value to transform, using the appropriate PropertyEditor + * from the enclosing BindTag. + *

The value can either be a plain value to transform (a hard-coded String + * value in a JSP or a JSP expression), or a JSP EL expression to be evaluated + * (transforming the result of the expression). + *

Like all of Spring's JSP tags, this tag is capable of parsing EL expressions + * itself, on any JSP version. Note, however, that EL expressions in a JSP 2.0 page + * will be evaluated by the JSP container, with the result getting passed in here. + * For this reason, the type of this property is Object (accepting any result + * object from a pre-evaluated expression) rather than String. + */ + public void setValue(Object value) { + this.value = value; + } + + /** + * Set PageContext attribute name under which to expose + * a variable that contains the result of the transformation. + * @see #setScope + * @see javax.servlet.jsp.PageContext#setAttribute + */ + public void setVar(String var) { + this.var = var; + } + + /** + * Set the scope to export the variable to. + * Default is SCOPE_PAGE ("page"). + * @see #setVar + * @see org.springframework.web.util.TagUtils#SCOPE_PAGE + * @see javax.servlet.jsp.PageContext#setAttribute + */ + public void setScope(String scope) { + this.scope = scope; + } + + + protected final int doStartTagInternal() throws JspException { + Object resolvedValue = this.value; + if (this.value instanceof String) { + String strValue = (String) this.value; + resolvedValue = ExpressionEvaluationUtils.evaluate("value", strValue, pageContext); + } + + if (resolvedValue != null) { + // Find the containing EditorAwareTag (e.g. BindTag), if applicable. + EditorAwareTag tag = (EditorAwareTag) TagSupport.findAncestorWithClass(this, EditorAwareTag.class); + if (tag == null) { + throw new JspException("TransformTag can only be used within EditorAwareTag (e.g. BindTag)"); + } + + // OK, let's obtain the editor... + String result = null; + PropertyEditor editor = tag.getEditor(); + if (editor != null) { + // If an editor was found, edit the value. + editor.setValue(resolvedValue); + result = editor.getAsText(); + } + else { + // Else, just do a toString. + result = resolvedValue.toString(); + } + result = isHtmlEscape() ? HtmlUtils.htmlEscape(result) : result; + String resolvedVar = ExpressionEvaluationUtils.evaluateString("var", this.var, pageContext); + if (resolvedVar != null) { + String resolvedScope = ExpressionEvaluationUtils.evaluateString("scope", this.scope, pageContext); + pageContext.setAttribute(resolvedVar, result, TagUtils.getScope(resolvedScope)); + } + else { + try { + // Else, just print it out. + pageContext.getOut().print(result); + } + catch (IOException ex) { + throw new JspException(ex); + } + } + } + + return SKIP_BODY; + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/AbstractCheckedElementTag.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/AbstractCheckedElementTag.java new file mode 100644 index 00000000000..9d88073d788 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/AbstractCheckedElementTag.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2008 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.web.servlet.tags.form; + +import java.beans.PropertyEditor; + +import javax.servlet.jsp.JspException; + +/** + * Abstract base class to provide common methods for + * implementing databinding-aware JSP tags for rendering an HTML 'input' + * element with a 'type' of 'checkbox' or 'radio'. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.5 + */ +public abstract class AbstractCheckedElementTag extends AbstractHtmlInputElementTag { + + /** + * Render the 'input(checkbox)' with the supplied value, marking the + * 'input' element as 'checked' if the supplied value matches the + * bound value. + */ + protected void renderFromValue(Object value, TagWriter tagWriter) throws JspException { + renderFromValue(value, value, tagWriter); + } + + /** + * Render the 'input(checkbox)' with the supplied value, marking the + * 'input' element as 'checked' if the supplied value matches the + * bound value. + */ + protected void renderFromValue(Object item, Object value, TagWriter tagWriter) throws JspException { + PropertyEditor editor = (value != null ? getBindStatus().findEditor(value.getClass()) : null); + tagWriter.writeAttribute("value", getDisplayString(value, editor)); + if (isOptionSelected(value) || (value != item && isOptionSelected(item))) { + tagWriter.writeAttribute("checked", "checked"); + } + } + + /** + * Determines whether the supplied value matched the selected value + * through delegating to {@link SelectedValueComparator#isSelected}. + */ + private boolean isOptionSelected(Object value) throws JspException { + return SelectedValueComparator.isSelected(getBindStatus(), value); + } + + /** + * Render the 'input(checkbox)' with the supplied value, marking + * the 'input' element as 'checked' if the supplied Boolean is + * true. + */ + protected void renderFromBoolean(Boolean boundValue, TagWriter tagWriter) throws JspException { + tagWriter.writeAttribute("value", "true"); + if (boundValue.booleanValue()) { + tagWriter.writeAttribute("checked", "checked"); + } + } + + /** + * Return a unique ID for the bound name within the current PageContext. + */ + protected String autogenerateId() throws JspException { + return TagIdGenerator.nextId(getName(), this.pageContext); + } + + + /** + * Writes the 'input' element to the supplied + * {@link org.springframework.web.servlet.tags.form.TagWriter}, + * marking it as 'checked' if appropriate. + */ + protected abstract int writeTagContent(TagWriter tagWriter) throws JspException; + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/AbstractDataBoundFormElementTag.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/AbstractDataBoundFormElementTag.java new file mode 100644 index 00000000000..d61eac968d4 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/AbstractDataBoundFormElementTag.java @@ -0,0 +1,227 @@ +/* + * Copyright 2002-2008 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.web.servlet.tags.form; + +import java.beans.PropertyEditor; + +import javax.servlet.jsp.JspException; +import javax.servlet.jsp.PageContext; + +import org.springframework.beans.PropertyAccessor; +import org.springframework.core.Conventions; +import org.springframework.util.StringUtils; +import org.springframework.web.servlet.support.BindStatus; +import org.springframework.web.servlet.tags.EditorAwareTag; +import org.springframework.web.servlet.tags.NestedPathTag; + +/** + * Base tag for all data-binding aware JSP form tags. + * + *

Provides the common {@link #setPath path} and {@link #setId id} properties. + * Provides sub-classes with utility methods for accessing the {@link BindStatus} + * of their bound value and also for {@link #writeOptionalAttribute interacting} + * with the {@link TagWriter}. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + */ +public abstract class AbstractDataBoundFormElementTag extends AbstractFormTag implements EditorAwareTag { + + /** + * Name of the exposed path variable within the scope of this tag: "nestedPath". + * Same value as {@link org.springframework.web.servlet.tags.NestedPathTag#NESTED_PATH_VARIABLE_NAME}. + */ + protected static final String NESTED_PATH_VARIABLE_NAME = NestedPathTag.NESTED_PATH_VARIABLE_NAME; + + /** + * The name of the {@link javax.servlet.jsp.PageContext} attribute under which the + * command object name is exposed. + * @deprecated as of Spring 2.5, in favor of {@link FormTag#MODEL_ATTRIBUTE_VARIABLE_NAME} + */ + public static final String COMMAND_NAME_VARIABLE_NAME = + Conventions.getQualifiedAttributeName(AbstractFormTag.class, "commandName"); + + + /** + * The property path from the {@link FormTag#setModelAttribute form object}. + */ + private String path; + + /** + * The value of the 'id' attribute. + */ + private String id; + + /** + * The {@link BindStatus} of this tag. + */ + private BindStatus bindStatus; + + + /** + * Set the property path from the {@link FormTag#setModelAttribute form object}. + * May be a runtime expression. + */ + public void setPath(String path) { + this.path = path; + } + + /** + * Get the {@link #evaluate resolved} property path for the + * {@link FormTag#setModelAttribute form object}. + */ + protected final String getPath() throws JspException { + String resolvedPath = (String) evaluate("path", this.path); + return (resolvedPath != null ? resolvedPath : ""); + } + + /** + * Set the value of the 'id' attribute. + *

May be a runtime expression; defaults to the value of {@link #getName()}. + * Note that the default value may not be valid for certain tags. + */ + public void setId(String id) { + this.id = id; + } + + /** + * Get the value of the 'id' attribute. + */ + public String getId() { + return this.id; + } + + + /** + * Writes the default set of attributes to the supplied {@link TagWriter}. + * Further abstract sub-classes should override this method to add in + * any additional default attributes but must remember + * to call the super method. + *

Concrete sub-classes should call this method when/if they want + * to render default attributes. + * @param tagWriter the {@link TagWriter} to which any attributes are to be written + */ + protected void writeDefaultAttributes(TagWriter tagWriter) throws JspException { + writeOptionalAttribute(tagWriter, "id", resolveId()); + writeOptionalAttribute(tagWriter, "name", getName()); + } + + /** + * Determine the 'id' attribute value for this tag, + * autogenerating one if none specified. + * @see #getId() + * @see #autogenerateId() + */ + protected String resolveId() throws JspException { + Object id = evaluate("id", getId()); + if (id != null) { + String idString = id.toString(); + return (StringUtils.hasText(idString) ? idString : null); + } + return autogenerateId(); + } + + /** + * Autogenerate the 'id' attribute value for this tag. + *

The default implementation simply delegates to {@link #getName()}, + * deleting invalid characters (such as "[" or "]"). + */ + protected String autogenerateId() throws JspException { + return StringUtils.deleteAny(getName(), "[]"); + } + + /** + * Get the value for the HTML 'name' attribute. + *

The default implementation simply delegates to + * {@link #getPropertyPath()} to use the property path as the name. + * For the most part this is desirable as it links with the server-side + * expectation for databinding. However, some subclasses may wish to change + * the value of the 'name' attribute without changing the bind path. + * @return the value for the HTML 'name' attribute + */ + protected String getName() throws JspException { + return getPropertyPath(); + } + + /** + * Get the {@link BindStatus} for this tag. + */ + protected BindStatus getBindStatus() throws JspException { + if (this.bindStatus == null) { + // HTML escaping in tags is performed by the ValueFormatter class. + String nestedPath = getNestedPath(); + String pathToUse = (nestedPath != null ? nestedPath + getPath() : getPath()); + if (pathToUse.endsWith(PropertyAccessor.NESTED_PROPERTY_SEPARATOR)) { + pathToUse = pathToUse.substring(0, pathToUse.length() - 1); + } + this.bindStatus = new BindStatus(getRequestContext(), pathToUse, false); + } + return this.bindStatus; + } + + /** + * Get the value of the nested path that may have been exposed by the + * {@link NestedPathTag}. + */ + protected String getNestedPath() { + return (String) this.pageContext.getAttribute(NESTED_PATH_VARIABLE_NAME, PageContext.REQUEST_SCOPE); + } + + /** + * Build the property path for this tag, including the nested path + * but not prefixed with the name of the form attribute. + * @see #getNestedPath() + * @see #getPath() + */ + protected String getPropertyPath() throws JspException { + String expression = getBindStatus().getExpression(); + return (expression != null ? expression : ""); + } + + /** + * Get the bound value. + * @see #getBindStatus() + */ + protected final Object getBoundValue() throws JspException { + return getBindStatus().getValue(); + } + + /** + * Get the {@link PropertyEditor}, if any, in use for value bound to this tag. + */ + protected PropertyEditor getPropertyEditor() throws JspException { + return getBindStatus().getEditor(); + } + + /** + * Exposes the {@link PropertyEditor} for {@link EditorAwareTag}. + *

Use {@link #getPropertyEditor()} for internal rendering purposes. + */ + public final PropertyEditor getEditor() throws JspException { + return getPropertyEditor(); + } + + /** + * Disposes of the {@link BindStatus} instance. + */ + public void doFinally() { + super.doFinally(); + this.bindStatus = null; + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/AbstractFormTag.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/AbstractFormTag.java new file mode 100644 index 00000000000..e078c81f3f5 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/AbstractFormTag.java @@ -0,0 +1,126 @@ +/* + * Copyright 2002-2007 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.web.servlet.tags.form; + +import java.beans.PropertyEditor; + +import javax.servlet.jsp.JspException; + +import org.springframework.util.ObjectUtils; +import org.springframework.web.servlet.tags.HtmlEscapingAwareTag; +import org.springframework.web.util.ExpressionEvaluationUtils; + +/** + * Base class for all JSP form tags. Provides utility methods for + * null-safe EL evaluation and for accessing and working with a {@link TagWriter}. + * + *

Subclasses should implement the {@link #writeTagContent(TagWriter)} to perform + * actual tag rendering. + * + *

Subclasses (or test classes) can override the {@link #createTagWriter()} method to + * redirect output to a {@link java.io.Writer} other than the {@link javax.servlet.jsp.JspWriter} + * associated with the current {@link javax.servlet.jsp.PageContext}. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + */ +public abstract class AbstractFormTag extends HtmlEscapingAwareTag { + + /** + * Evaluate the supplied value for the supplied attribute name. If the supplied value + * is null then null is returned, otherwise evaluation is + * handled using {@link ExpressionEvaluationUtils#evaluate(String, String, javax.servlet.jsp.PageContext)}. + */ + protected Object evaluate(String attributeName, Object value) throws JspException { + if (value instanceof String) { + return ExpressionEvaluationUtils.evaluate(attributeName, (String) value, this.pageContext); + } + else { + return value; + } + } + + /** + * Optionally writes the supplied value under the supplied attribute name into the supplied + * {@link TagWriter}. In this case, the supplied value is {@link #evaluate evaluated} first + * and then the {@link ObjectUtils#getDisplayString String representation} is written as the + * attribute value. If the resultant String representation is null + * or empty, no attribute is written. + * @see TagWriter#writeOptionalAttributeValue(String, String) + */ + protected final void writeOptionalAttribute(TagWriter tagWriter, String attributeName, String value) + throws JspException { + + if (value != null) { + tagWriter.writeOptionalAttributeValue(attributeName, getDisplayString(evaluate(attributeName, value))); + } + } + + /** + * Create the {@link TagWriter} which all output will be written to. By default, + * the {@link TagWriter} writes its output to the {@link javax.servlet.jsp.JspWriter} + * for the current {@link javax.servlet.jsp.PageContext}. Subclasses may choose to + * change the {@link java.io.Writer} to which output is actually written. + */ + protected TagWriter createTagWriter() { + return new TagWriter(this.pageContext); + } + + /** + * Provide a simple template method that calls {@link #createTagWriter()} and passes + * the created {@link TagWriter} to the {@link #writeTagContent(TagWriter)} method. + * @return the value returned by {@link #writeTagContent(TagWriter)} + */ + protected final int doStartTagInternal() throws Exception { + return writeTagContent(createTagWriter()); + } + + /** + * Get the display value of the supplied Object, HTML escaped + * as required. This version is not {@link PropertyEditor}-aware. + */ + protected String getDisplayString(Object value) { + return ValueFormatter.getDisplayString(value, isHtmlEscape()); + } + + /** + * Get the display value of the supplied Object, HTML escaped + * as required. If the supplied value is not a {@link String} and the supplied + * {@link PropertyEditor} is not null then the {@link PropertyEditor} is used + * to obtain the display value. + */ + protected String getDisplayString(Object value, PropertyEditor propertyEditor) { + return ValueFormatter.getDisplayString(value, propertyEditor, isHtmlEscape()); + } + + /** + * Overridden to default to true in case of no explicit default given. + */ + protected boolean isDefaultHtmlEscape() { + Boolean defaultHtmlEscape = getRequestContext().getDefaultHtmlEscape(); + return (defaultHtmlEscape == null || defaultHtmlEscape.booleanValue()); + } + + + /** + * Subclasses should implement this method to perform tag content rendering. + * @return valid tag render instruction as per {@link javax.servlet.jsp.tagext.Tag#doStartTag()}. + */ + protected abstract int writeTagContent(TagWriter tagWriter) throws JspException; + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/AbstractHtmlElementBodyTag.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/AbstractHtmlElementBodyTag.java new file mode 100644 index 00000000000..c66edb12168 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/AbstractHtmlElementBodyTag.java @@ -0,0 +1,156 @@ +/* + * Copyright 2002-2008 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.web.servlet.tags.form; + +import java.io.IOException; + +import javax.servlet.jsp.JspException; +import javax.servlet.jsp.tagext.BodyContent; +import javax.servlet.jsp.tagext.BodyTag; + +import org.springframework.util.StringUtils; + +/** + * Convenient super class for many html tags that render content using the databinding + * features of the {@link AbstractHtmlElementTag AbstractHtmlElementTag}. The only thing sub tags + * need to do is override {@link #renderDefaultContent(TagWriter)}. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + */ +public abstract class AbstractHtmlElementBodyTag extends AbstractHtmlElementTag implements BodyTag { + + private BodyContent bodyContent; + + private TagWriter tagWriter; + + + protected int writeTagContent(TagWriter tagWriter) throws JspException { + onWriteTagContent(); + this.tagWriter = tagWriter; + if (shouldRender()) { + exposeAttributes(); + return EVAL_BODY_BUFFERED; + } + else { + return SKIP_BODY; + } + } + + /** + * If {@link #shouldRender rendering}, flush any buffered + * {@link BodyContent} or, if no {@link BodyContent} is supplied, + * {@link #renderDefaultContent render the default content}. + * @return Tag#EVAL_PAGE + */ + public int doEndTag() throws JspException { + if (shouldRender()) { + if (this.bodyContent != null && StringUtils.hasText(this.bodyContent.getString())) { + renderFromBodyContent(this.bodyContent, this.tagWriter); + } + else { + renderDefaultContent(this.tagWriter); + } + } + return EVAL_PAGE; + } + + /** + * Render the tag contents based on the supplied {@link BodyContent}. + *

The default implementation simply {@link #flushBufferedBodyContent flushes} + * the {@link BodyContent} directly to the output. Subclasses may choose to + * override this to add additional content to the output. + */ + protected void renderFromBodyContent(BodyContent bodyContent, TagWriter tagWriter) throws JspException { + flushBufferedBodyContent(this.bodyContent); + } + + /** + * Clean up any attributes and stored resources. + */ + public void doFinally() { + super.doFinally(); + removeAttributes(); + this.tagWriter = null; + this.bodyContent = null; + } + + + //--------------------------------------------------------------------- + // Template methods + //--------------------------------------------------------------------- + + /** + * Called at the start of {@link #writeTagContent} allowing subclasses to perform + * any precondition checks or setup tasks that might be necessary. + */ + protected void onWriteTagContent() { + } + + /** + * Should rendering of this tag proceed at all. Returns 'true' by default + * causing rendering to occur always, Subclasses can override this if they + * provide conditional rendering. + */ + protected boolean shouldRender() throws JspException { + return true; + } + + /** + * Called during {@link #writeTagContent} allowing subclasses to add any attributes to the + * {@link javax.servlet.jsp.PageContext} as needed. + */ + protected void exposeAttributes() throws JspException { + } + + /** + * Called by {@link #doFinally} allowing subclasses to remove any attributes from the + * {@link javax.servlet.jsp.PageContext} as needed. + */ + protected void removeAttributes() { + } + + /** + * The user customised the output of the error messages - flush the + * buffered content into the main {@link javax.servlet.jsp.JspWriter}. + */ + protected void flushBufferedBodyContent(BodyContent bodyContent) throws JspException { + try { + bodyContent.writeOut(bodyContent.getEnclosingWriter()); + } + catch (IOException e) { + throw new JspException("Unable to write buffered body content.", e); + } + } + + protected abstract void renderDefaultContent(TagWriter tagWriter) throws JspException; + + + //--------------------------------------------------------------------- + // BodyTag implementation + //--------------------------------------------------------------------- + + public void doInitBody() throws JspException { + // no op + } + + public void setBodyContent(BodyContent bodyContent) { + this.bodyContent = bodyContent; + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/AbstractHtmlElementTag.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/AbstractHtmlElementTag.java new file mode 100644 index 00000000000..55a2267e499 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/AbstractHtmlElementTag.java @@ -0,0 +1,420 @@ +/* + * Copyright 2002-2007 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.web.servlet.tags.form; + +import javax.servlet.jsp.JspException; + +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Base class for databinding-aware JSP tags that render HTML element. Provides + * a set of properties corresponding to the set of HTML attributes that are common + * across elements. + * + * @author Rob Harrop + * @since 2.0 + */ +public abstract class AbstractHtmlElementTag extends AbstractDataBoundFormElementTag { + + public static final String CLASS_ATTRIBUTE = "class"; + + public static final String STYLE_ATTRIBUTE = "style"; + + public static final String LANG_ATTRIBUTE = "lang"; + + public static final String TITLE_ATTRIBUTE = "title"; + + public static final String DIR_ATTRIBUTE = "dir"; + + public static final String TABINDEX_ATTRIBUTE = "tabindex"; + + public static final String ONCLICK_ATTRIBUTE = "onclick"; + + public static final String ONDBLCLICK_ATTRIBUTE = "ondblclick"; + + public static final String ONMOUSEDOWN_ATTRIBUTE = "onmousedown"; + + public static final String ONMOUSEUP_ATTRIBUTE = "onmouseup"; + + public static final String ONMOUSEOVER_ATTRIBUTE = "onmouseover"; + + public static final String ONMOUSEMOVE_ATTRIBUTE = "onmousemove"; + + public static final String ONMOUSEOUT_ATTRIBUTE = "onmouseout"; + + public static final String ONKEYPRESS_ATTRIBUTE = "onkeypress"; + + public static final String ONKEYUP_ATTRIBUTE = "onkeyup"; + + public static final String ONKEYDOWN_ATTRIBUTE = "onkeydown"; + + + private String cssClass; + + private String cssErrorClass; + + private String cssStyle; + + private String lang; + + private String title; + + private String dir; + + private String tabindex; + + private String onclick; + + private String ondblclick; + + private String onmousedown; + + private String onmouseup; + + private String onmouseover; + + private String onmousemove; + + private String onmouseout; + + private String onkeypress; + + private String onkeyup; + + private String onkeydown; + + + /** + * Set the value of the 'class' attribute. + * May be a runtime expression. + */ + public void setCssClass(String cssClass) { + this.cssClass = cssClass; + } + + /** + * Get the value of the 'class' attribute. + * May be a runtime expression. + */ + protected String getCssClass() { + return this.cssClass; + } + + /** + * The CSS class to use when the field bound to a particular tag has errors. + * May be a runtime expression. + */ + public void setCssErrorClass(String cssErrorClass) { + this.cssErrorClass = cssErrorClass; + } + + /** + * The CSS class to use when the field bound to a particular tag has errors. + * May be a runtime expression. + */ + protected String getCssErrorClass() { + return this.cssErrorClass; + } + + /** + * Set the value of the 'style' attribute. + * May be a runtime expression. + */ + public void setCssStyle(String cssStyle) { + this.cssStyle = cssStyle; + } + + /** + * Get the value of the 'style' attribute. + * May be a runtime expression. + */ + protected String getCssStyle() { + return this.cssStyle; + } + + /** + * Set the value of the 'lang' attribute. + * May be a runtime expression. + */ + public void setLang(String lang) { + this.lang = lang; + } + + /** + * Get the value of the 'lang' attribute. + * May be a runtime expression. + */ + protected String getLang() { + return this.lang; + } + + /** + * Set the value of the 'title' attribute. + * May be a runtime expression. + */ + public void setTitle(String title) { + this.title = title; + } + + /** + * Get the value of the 'title' attribute. + * May be a runtime expression. + */ + protected String getTitle() { + return this.title; + } + + /** + * Set the value of the 'dir' attribute. + * May be a runtime expression. + */ + public void setDir(String dir) { + this.dir = dir; + } + + /** + * Get the value of the 'dir' attribute. + * May be a runtime expression. + */ + protected String getDir() { + return this.dir; + } + + /** + * Set the value of the 'tabindex' attribute. + * May be a runtime expression. + */ + public void setTabindex(String tabindex) { + this.tabindex = tabindex; + } + + /** + * Get the value of the 'tabindex' attribute. + * May be a runtime expression. + */ + protected String getTabindex() { + return this.tabindex; + } + + /** + * Set the value of the 'onclick' attribute. + * May be a runtime expression. + */ + public void setOnclick(String onclick) { + this.onclick = onclick; + } + + /** + * Get the value of the 'onclick' attribute. + * May be a runtime expression. + */ + protected String getOnclick() { + return this.onclick; + } + + /** + * Set the value of the 'ondblclick' attribute. + * May be a runtime expression. + */ + public void setOndblclick(String ondblclick) { + this.ondblclick = ondblclick; + } + + /** + * Get the value of the 'ondblclick' attribute. + * May be a runtime expression. + */ + protected String getOndblclick() { + return this.ondblclick; + } + + /** + * Set the value of the 'onmousedown' attribute. + * May be a runtime expression. + */ + public void setOnmousedown(String onmousedown) { + this.onmousedown = onmousedown; + } + + /** + * Get the value of the 'onmousedown' attribute. + * May be a runtime expression. + */ + protected String getOnmousedown() { + return this.onmousedown; + } + + /** + * Set the value of the 'onmouseup' attribute. + * May be a runtime expression. + */ + public void setOnmouseup(String onmouseup) { + this.onmouseup = onmouseup; + } + + /** + * Get the value of the 'onmouseup' attribute. + * May be a runtime expression. + */ + protected String getOnmouseup() { + return this.onmouseup; + } + + /** + * Set the value of the 'onmouseover' attribute. + * May be a runtime expression. + */ + public void setOnmouseover(String onmouseover) { + this.onmouseover = onmouseover; + } + + /** + * Get the value of the 'onmouseover' attribute. + * May be a runtime expression. + */ + protected String getOnmouseover() { + return this.onmouseover; + } + + /** + * Set the value of the 'onmousemove' attribute. + * May be a runtime expression. + */ + public void setOnmousemove(String onmousemove) { + this.onmousemove = onmousemove; + } + + /** + * Get the value of the 'onmousemove' attribute. + * May be a runtime expression. + */ + protected String getOnmousemove() { + return this.onmousemove; + } + + /** + * Set the value of the 'onmouseout' attribute. + * May be a runtime expression. + */ + public void setOnmouseout(String onmouseout) { + this.onmouseout = onmouseout; + } + /** + * Get the value of the 'onmouseout' attribute. + * May be a runtime expression. + */ + protected String getOnmouseout() { + return this.onmouseout; + } + + /** + * Set the value of the 'onkeypress' attribute. + * May be a runtime expression. + */ + public void setOnkeypress(String onkeypress) { + this.onkeypress = onkeypress; + } + + /** + * Get the value of the 'onkeypress' attribute. + * May be a runtime expression. + */ + protected String getOnkeypress() { + return this.onkeypress; + } + + /** + * Set the value of the 'onkeyup' attribute. + * May be a runtime expression. + */ + public void setOnkeyup(String onkeyup) { + this.onkeyup = onkeyup; + } + + /** + * Get the value of the 'onkeyup' attribute. + * May be a runtime expression. + */ + protected String getOnkeyup() { + return this.onkeyup; + } + + /** + * Set the value of the 'onkeydown' attribute. + * May be a runtime expression. + */ + public void setOnkeydown(String onkeydown) { + this.onkeydown = onkeydown; + } + + /** + * Get the value of the 'onkeydown' attribute. + * May be a runtime expression. + */ + protected String getOnkeydown() { + return this.onkeydown; + } + + + /** + * Writes the default attributes configured via this base class to the supplied {@link TagWriter}. + * Subclasses should call this when they want the base attribute set to be written to the output. + */ + protected void writeDefaultAttributes(TagWriter tagWriter) throws JspException { + super.writeDefaultAttributes(tagWriter); + writeOptionalAttributes(tagWriter); + } + + /** + * Writes the optional attributes configured via this base class to the supplied {@link TagWriter}. + * Called by {@link #writeDefaultAttributes(TagWriter)}. + */ + protected void writeOptionalAttributes(TagWriter tagWriter) throws JspException { + tagWriter.writeOptionalAttributeValue(CLASS_ATTRIBUTE, resolveCssClass()); + tagWriter.writeOptionalAttributeValue(STYLE_ATTRIBUTE, + ObjectUtils.getDisplayString(evaluate("cssStyle", getCssStyle()))); + writeOptionalAttribute(tagWriter, LANG_ATTRIBUTE, getLang()); + writeOptionalAttribute(tagWriter, TITLE_ATTRIBUTE, getTitle()); + writeOptionalAttribute(tagWriter, DIR_ATTRIBUTE, getDir()); + writeOptionalAttribute(tagWriter, TABINDEX_ATTRIBUTE, getTabindex()); + writeOptionalAttribute(tagWriter, ONCLICK_ATTRIBUTE, getOnclick()); + writeOptionalAttribute(tagWriter, ONDBLCLICK_ATTRIBUTE, getOndblclick()); + writeOptionalAttribute(tagWriter, ONMOUSEDOWN_ATTRIBUTE, getOnmousedown()); + writeOptionalAttribute(tagWriter, ONMOUSEUP_ATTRIBUTE, getOnmouseup()); + writeOptionalAttribute(tagWriter, ONMOUSEOVER_ATTRIBUTE, getOnmouseover()); + writeOptionalAttribute(tagWriter, ONMOUSEMOVE_ATTRIBUTE, getOnmousemove()); + writeOptionalAttribute(tagWriter, ONMOUSEOUT_ATTRIBUTE, getOnmouseout()); + writeOptionalAttribute(tagWriter, ONKEYPRESS_ATTRIBUTE, getOnkeypress()); + writeOptionalAttribute(tagWriter, ONKEYUP_ATTRIBUTE, getOnkeyup()); + writeOptionalAttribute(tagWriter, ONKEYDOWN_ATTRIBUTE, getOnkeydown()); + } + + /** + * Gets the appropriate CSS class to use based on the state of the current + * {@link org.springframework.web.servlet.support.BindStatus} object. + */ + protected String resolveCssClass() throws JspException { + if (getBindStatus().isError() && StringUtils.hasText(getCssErrorClass())) { + return ObjectUtils.getDisplayString(evaluate("cssErrorClass", getCssErrorClass())); + } + else { + return ObjectUtils.getDisplayString(evaluate("cssClass", getCssClass())); + } + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/AbstractHtmlInputElementTag.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/AbstractHtmlInputElementTag.java new file mode 100644 index 00000000000..1703839e82a --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/AbstractHtmlInputElementTag.java @@ -0,0 +1,207 @@ +/* + * Copyright 2002-2007 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.web.servlet.tags.form; + +import javax.servlet.jsp.JspException; + +/** + * Base class for databinding-aware JSP tags that render HTML form input element. + * + *

Provides a set of properties corresponding to the set of HTML attributes + * that are common across form input elements. + * + * @author Rob Harrop + * @author Rick Evans + * @author Juergen Hoeller + * @since 2.0 + */ +public abstract class AbstractHtmlInputElementTag extends AbstractHtmlElementTag { + + /** + * The name of the 'onfocus' attribute. + */ + public static final String ONFOCUS_ATTRIBUTE = "onfocus"; + + /** + * The name of the 'onblur' attribute. + */ + public static final String ONBLUR_ATTRIBUTE = "onblur"; + + /** + * The name of the 'onchange' attribute. + */ + public static final String ONCHANGE_ATTRIBUTE = "onchange"; + + /** + * The name of the 'accesskey' attribute. + */ + public static final String ACCESSKEY_ATTRIBUTE = "accesskey"; + + /** + * The name of the 'disabled' attribute. + */ + public static final String DISABLED_ATTRIBUTE = "disabled"; + + /** + * The name of the 'readonly' attribute. + */ + public static final String READONLY_ATTRIBUTE = "readonly"; + + + private String onfocus; + + private String onblur; + + private String onchange; + + private String accesskey; + + private String disabled; + + private String readonly; + + + /** + * Set the value of the 'onfocus' attribute. + * May be a runtime expression. + */ + public void setOnfocus(String onfocus) { + this.onfocus = onfocus; + } + + /** + * Get the value of the 'onfocus' attribute. + */ + protected String getOnfocus() { + return this.onfocus; + } + + /** + * Set the value of the 'onblur' attribute. + * May be a runtime expression. + */ + public void setOnblur(String onblur) { + this.onblur = onblur; + } + + /** + * Get the value of the 'onblur' attribute. + */ + protected String getOnblur() { + return this.onblur; + } + + /** + * Set the value of the 'onchange' attribute. + * May be a runtime expression. + */ + public void setOnchange(String onchange) { + this.onchange = onchange; + } + + /** + * Get the value of the 'onchange' attribute. + */ + protected String getOnchange() { + return this.onchange; + } + + /** + * Set the value of the 'accesskey' attribute. + * May be a runtime expression. + */ + public void setAccesskey(String accesskey) { + this.accesskey = accesskey; + } + + /** + * Get the value of the 'accesskey' attribute. + */ + protected String getAccesskey() { + return this.accesskey; + } + + /** + * Set the value of the 'disabled' attribute. + * May be a runtime expression. + */ + public void setDisabled(String disabled) { + this.disabled = disabled; + } + + /** + * Get the value of the 'disabled' attribute. + */ + protected String getDisabled() { + return this.disabled; + } + + /** + * Sets the value of the 'readonly' attribute. + * May be a runtime expression. + * @see #isReadonly() + */ + public void setReadonly(String readonly) { + this.readonly = readonly; + } + + /** + * Gets the value of the 'readonly' attribute. + * May be a runtime expression. + * @see #isReadonly() + */ + protected String getReadonly() { + return this.readonly; + } + + + /** + * Adds input-specific optional attributes as defined by this base class. + */ + protected void writeOptionalAttributes(TagWriter tagWriter) throws JspException { + super.writeOptionalAttributes(tagWriter); + + writeOptionalAttribute(tagWriter, ONFOCUS_ATTRIBUTE, getOnfocus()); + writeOptionalAttribute(tagWriter, ONBLUR_ATTRIBUTE, getOnblur()); + writeOptionalAttribute(tagWriter, ONCHANGE_ATTRIBUTE, getOnchange()); + writeOptionalAttribute(tagWriter, ACCESSKEY_ATTRIBUTE, getAccesskey()); + if (isDisabled()) { + tagWriter.writeAttribute(DISABLED_ATTRIBUTE, "disabled"); + } + if (isReadonly()) { + writeOptionalAttribute(tagWriter, READONLY_ATTRIBUTE, "readonly"); + } + } + + /** + * Is the current HTML tag disabled? + */ + protected boolean isDisabled() throws JspException { + return "true".equals(evaluate(DISABLED_ATTRIBUTE, getDisabled())); + } + + /** + * Is the current HTML tag readonly? + *

Note: some {@link AbstractHtmlInputElementTag} subclasses (such a those + * for checkboxes and radiobuttons) may contain readonly attributes, but are + * not affected by them since their values don't change (only their status does.) + */ + protected boolean isReadonly() throws JspException { + return "true".equals(evaluate(READONLY_ATTRIBUTE, getReadonly())); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/AbstractMultiCheckedElementTag.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/AbstractMultiCheckedElementTag.java new file mode 100644 index 00000000000..69895eb210c --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/AbstractMultiCheckedElementTag.java @@ -0,0 +1,280 @@ +/* + * Copyright 2002-2008 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.web.servlet.tags.form; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; + +import javax.servlet.jsp.JspException; + +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.PropertyAccessorFactory; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Abstract base class to provide common methods for implementing + * databinding-aware JSP tags for rendering multiple + * HTML 'input' elements with a 'type' + * of 'checkbox' or 'radio'. + * + * @author Juergen Hoeller + * @since 2.5.2 + */ +public abstract class AbstractMultiCheckedElementTag extends AbstractCheckedElementTag { + + /** + * The HTML 'span' tag. + */ + private static final String SPAN_TAG = "span"; + + + /** + * The {@link java.util.Collection}, {@link java.util.Map} or array of objects + * used to generate the 'input type="checkbox/radio"' tags. + */ + private Object items; + + /** + * The name of the property mapped to the 'value' attribute + * of the 'input type="checkbox/radio"' tag. + */ + private String itemValue; + + /** + * The value to be displayed as part of the 'input type="checkbox/radio"' tag. + */ + private String itemLabel; + + /** + * The HTML element used to enclose the 'input type="checkbox/radio"' tag. + */ + private String element = SPAN_TAG; + + /** + * Delimiter to use between each 'input type="checkbox/radio"' tags. + */ + private String delimiter; + + + /** + * Set the {@link java.util.Collection}, {@link java.util.Map} or array of objects + * used to generate the 'input type="checkbox/radio"' tags. + *

Typically a runtime expression. + * @param items said items + */ + public void setItems(Object items) { + Assert.notNull(items, "'items' must not be null"); + this.items = items; + } + + /** + * Get the {@link java.util.Collection}, {@link java.util.Map} or array of objects + * used to generate the 'input type="checkbox/radio"' tags. + */ + protected Object getItems() { + return this.items; + } + + /** + * Set the name of the property mapped to the 'value' attribute + * of the 'input type="checkbox/radio"' tag. + *

May be a runtime expression. + */ + public void setItemValue(String itemValue) { + Assert.hasText(itemValue, "'itemValue' must not be empty"); + this.itemValue = itemValue; + } + + protected String getItemValue() { + return this.itemValue; + } + + /** + * Set the value to be displayed as part + * of the 'input type="checkbox/radio"' tag. + *

May be a runtime expression. + */ + public void setItemLabel(String itemLabel) { + Assert.hasText(itemLabel, "'itemLabel' must not be empty"); + this.itemLabel = itemLabel; + } + + /** + * Get the value to be displayed as part + * of the 'input type="checkbox/radio"' tag. + */ + protected String getItemLabel() { + return this.itemLabel; + } + + /** + * Set the delimiter to be used between each + * 'input type="checkbox/radio"' tag. + *

By default, there is no delimiter. + */ + public void setDelimiter(String delimiter) { + this.delimiter = delimiter; + } + + /** + * Return the delimiter to be used between each + * 'input type="radio"' tag. + */ + public String getDelimiter() { + return this.delimiter; + } + + /** + * Set the HTML element used to enclose the + * 'input type="checkbox/radio"' tag. + *

Defaults to an HTML '<span/>' tag. + */ + public void setElement(String element) { + Assert.hasText(element, "'element' cannot be null or blank"); + this.element = element; + } + + /** + * Get the HTML element used to enclose + * 'input type="checkbox/radio"' tag. + */ + public String getElement() { + return this.element; + } + + + /** + * Appends a counter to a specified id as well, + * since we're dealing with multiple HTML elements. + */ + protected String resolveId() throws JspException { + Object id = evaluate("id", getId()); + if (id != null) { + String idString = id.toString(); + return (StringUtils.hasText(idString) ? TagIdGenerator.nextId(idString, this.pageContext) : null); + } + return autogenerateId(); + } + + /** + * Renders the 'input type="radio"' element with the configured + * {@link #setItems(Object)} values. Marks the element as checked if the + * value matches the bound value. + */ + protected int writeTagContent(TagWriter tagWriter) throws JspException { + Object items = getItems(); + Object itemsObject = (items instanceof String ? evaluate("items", (String) items) : items); + + String itemValue = getItemValue(); + String itemLabel = getItemLabel(); + String valueProperty = + (itemValue != null ? ObjectUtils.getDisplayString(evaluate("itemValue", itemValue)) : null); + String labelProperty = + (itemLabel != null ? ObjectUtils.getDisplayString(evaluate("itemLabel", itemLabel)) : null); + + if (itemsObject == null) { + throw new IllegalArgumentException("Attribute 'items' is required and must be a Collection, an Array or a Map"); + } + + if (itemsObject.getClass().isArray()) { + Object[] itemsArray = (Object[]) itemsObject; + for (int i = 0; i < itemsArray.length; i++) { + Object item = itemsArray[i]; + writeObjectEntry(tagWriter, valueProperty, labelProperty, item, i); + } + } + else if (itemsObject instanceof Collection) { + final Collection optionCollection = (Collection) itemsObject; + int itemIndex = 0; + for (Iterator it = optionCollection.iterator(); it.hasNext(); itemIndex++) { + Object item = it.next(); + writeObjectEntry(tagWriter, valueProperty, labelProperty, item, itemIndex); + } + } + else if (itemsObject instanceof Map) { + final Map optionMap = (Map) itemsObject; + int itemIndex = 0; + for (Iterator it = optionMap.entrySet().iterator(); it.hasNext(); itemIndex++) { + Map.Entry entry = (Map.Entry) it.next(); + writeMapEntry(tagWriter, valueProperty, labelProperty, entry, itemIndex); + } + } + else { + throw new IllegalArgumentException("Attribute 'items' must be a Collection, an Array or a Map"); + } + + return SKIP_BODY; + } + + private void writeObjectEntry(TagWriter tagWriter, String valueProperty, + String labelProperty, Object item, int itemIndex) throws JspException { + + BeanWrapper wrapper = PropertyAccessorFactory.forBeanPropertyAccess(item); + Object renderValue = (valueProperty != null ? wrapper.getPropertyValue(valueProperty) : item); + Object renderLabel = (labelProperty != null ? wrapper.getPropertyValue(labelProperty) : item); + writeElementTag(tagWriter, item, renderValue, renderLabel, itemIndex); + } + + private void writeMapEntry(TagWriter tagWriter, String valueProperty, + String labelProperty, Map.Entry entry, int itemIndex) throws JspException { + + Object mapKey = entry.getKey(); + Object mapValue = entry.getValue(); + BeanWrapper mapKeyWrapper = PropertyAccessorFactory.forBeanPropertyAccess(mapKey); + BeanWrapper mapValueWrapper = PropertyAccessorFactory.forBeanPropertyAccess(mapValue); + Object renderValue = (valueProperty != null ? + mapKeyWrapper.getPropertyValue(valueProperty) : mapKey.toString()); + Object renderLabel = (labelProperty != null ? + mapValueWrapper.getPropertyValue(labelProperty) : mapValue.toString()); + writeElementTag(tagWriter, mapKey, renderValue, renderLabel, itemIndex); + } + + private void writeElementTag(TagWriter tagWriter, Object item, Object value, Object label, int itemIndex) + throws JspException { + + tagWriter.startTag(getElement()); + if (itemIndex > 0) { + Object resolvedDelimiter = evaluate("delimiter", getDelimiter()); + if (resolvedDelimiter != null) { + tagWriter.appendValue(resolvedDelimiter.toString()); + } + } + tagWriter.startTag("input"); + String id = resolveId(); + writeOptionalAttribute(tagWriter, "id", id); + writeOptionalAttribute(tagWriter, "name", getName()); + writeOptionalAttributes(tagWriter); + tagWriter.writeAttribute("type", getInputType()); + renderFromValue(item, value, tagWriter); + tagWriter.endTag(); + tagWriter.startTag("label"); + tagWriter.writeAttribute("for", id); + tagWriter.appendValue(label.toString()); + tagWriter.endTag(); + tagWriter.endTag(); + } + + /** + * Return the type of the HTML input element to generate: + * "checkbox" or "radio". + */ + protected abstract String getInputType(); + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/AbstractSingleCheckedElementTag.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/AbstractSingleCheckedElementTag.java new file mode 100644 index 00000000000..7d57fb46db6 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/AbstractSingleCheckedElementTag.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2008 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.web.servlet.tags.form; + +import javax.servlet.jsp.JspException; + +/** + * Abstract base class to provide common methods for implementing + * databinding-aware JSP tags for rendering a single + * HTML 'input' element with a 'type' + * of 'checkbox' or 'radio'. + * + * @author Juergen Hoeller + * @since 2.5.2 + */ +public abstract class AbstractSingleCheckedElementTag extends AbstractCheckedElementTag { + + /** + * The value of the 'value' attribute. + */ + private Object value; + + /** + * The value of the 'label' attribute. + */ + private Object label; + + + /** + * Set the value of the 'value' attribute. + * May be a runtime expression. + */ + public void setValue(Object value) { + this.value = value; + } + + /** + * Get the value of the 'value' attribute. + */ + protected Object getValue() { + return this.value; + } + + /** + * Set the value of the 'label' attribute. + * May be a runtime expression. + */ + public void setLabel(Object label) { + this.label = label; + } + + /** + * Get the value of the 'label' attribute. + */ + protected Object getLabel() { + return this.label; + } + + + /** + * Renders the 'input(radio)' element with the configured + * {@link #setValue(Object) value}. Marks the element as checked if the + * value matches the {@link #getValue bound value}. + */ + protected int writeTagContent(TagWriter tagWriter) throws JspException { + tagWriter.startTag("input"); + String id = resolveId(); + writeOptionalAttribute(tagWriter, "id", id); + writeOptionalAttribute(tagWriter, "name", getName()); + writeOptionalAttributes(tagWriter); + writeTagDetails(tagWriter); + tagWriter.endTag(); + + Object resolvedLabel = evaluate("label", getLabel()); + if (resolvedLabel != null) { + tagWriter.startTag("label"); + tagWriter.writeAttribute("for", id); + tagWriter.appendValue(resolvedLabel.toString()); + tagWriter.endTag(); + } + + return SKIP_BODY; + } + + /** + * Write the details for the given primary tag: + * i.e. special attributes and the tag's value. + */ + protected abstract void writeTagDetails(TagWriter tagWriter) throws JspException; + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/CheckboxTag.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/CheckboxTag.java new file mode 100644 index 00000000000..29ba37f508a --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/CheckboxTag.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2008 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.web.servlet.tags.form; + +import java.util.Collection; + +import javax.servlet.jsp.JspException; + +import org.springframework.web.bind.WebDataBinder; + +/** + * Databinding-aware JSP tag for rendering an HTML 'input' + * element with a 'type' of 'checkbox'. + * + *

May be used in one of three different approaches depending on the + * type of the {@link #getValue bound value}. + * + *

Approach One

+ * When the bound value is of type {@link Boolean} then the 'input(checkbox)' + * is marked as 'checked' if the bound value is true. The 'value' + * attribute corresponds to the resolved value of the {@link #setValue(Object) value} property. + *

Approach Two

+ * When the bound value is of type {@link Collection} then the 'input(checkbox)' + * is marked as 'checked' if the configured {@link #setValue(Object) value} is present in + * the bound {@link Collection}. + *

Approach Three

+ * For any other bound value type, the 'input(checkbox)' is marked as 'checked' + * if the the configured {@link #setValue(Object) value} is equal to the bound value. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + */ +public class CheckboxTag extends AbstractSingleCheckedElementTag { + + protected int writeTagContent(TagWriter tagWriter) throws JspException { + super.writeTagContent(tagWriter); + + if (!isDisabled()) { + // Write out the 'field was present' marker. + tagWriter.startTag("input"); + tagWriter.writeAttribute("type", "hidden"); + tagWriter.writeAttribute("name", WebDataBinder.DEFAULT_FIELD_MARKER_PREFIX + getName()); + tagWriter.writeAttribute("value", "on"); + tagWriter.endTag(); + } + + return SKIP_BODY; + } + + protected void writeTagDetails(TagWriter tagWriter) throws JspException { + tagWriter.writeAttribute("type", "checkbox"); + + Object boundValue = getBoundValue(); + Class valueType = getBindStatus().getValueType(); + + if (Boolean.class.equals(valueType) || boolean.class.equals(valueType)) { + // the concrete type may not be a Boolean - can be String + if (boundValue instanceof String) { + boundValue = Boolean.valueOf((String) boundValue); + } + Boolean booleanValue = (boundValue != null ? (Boolean) boundValue : Boolean.FALSE); + renderFromBoolean(booleanValue, tagWriter); + } + + else { + Object value = getValue(); + if (value == null) { + throw new IllegalArgumentException("Attribute 'value' is required when binding to non-boolean values"); + } + Object resolvedValue = (value instanceof String ? evaluate("value", (String) value) : value); + renderFromValue(resolvedValue, tagWriter); + } + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/CheckboxesTag.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/CheckboxesTag.java new file mode 100644 index 00000000000..0114d8c2319 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/CheckboxesTag.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2008 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.web.servlet.tags.form; + +import javax.servlet.jsp.JspException; + +import org.springframework.web.bind.WebDataBinder; + +/** + * Databinding-aware JSP tag for rendering multiple HTML 'input' + * elements with a 'type' of 'checkbox'. + * + *

Intended to be used with a Collection as the {@link #getItems()} bound value}. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @author Mark Fisher + * @since 2.5 + */ +public class CheckboxesTag extends AbstractMultiCheckedElementTag { + + protected int writeTagContent(TagWriter tagWriter) throws JspException { + super.writeTagContent(tagWriter); + + if (!isDisabled()) { + // Write out the 'field was present' marker. + tagWriter.startTag("input"); + tagWriter.writeAttribute("type", "hidden"); + tagWriter.writeAttribute("name", WebDataBinder.DEFAULT_FIELD_MARKER_PREFIX + getName()); + tagWriter.writeAttribute("value", "on"); + tagWriter.endTag(); + } + + return SKIP_BODY; + } + + protected String getInputType() { + return "checkbox"; + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/ErrorsTag.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/ErrorsTag.java new file mode 100644 index 00000000000..f216d05205c --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/ErrorsTag.java @@ -0,0 +1,191 @@ +/* + * Copyright 2002-2008 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.web.servlet.tags.form; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.servlet.jsp.JspException; +import javax.servlet.jsp.PageContext; +import javax.servlet.jsp.tagext.BodyTag; + +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Form tag for displaying errors for a particular field or object. + * + *

This tag supports three main usage patterns: + * + *

    + *
  1. Field only - set 'path' to the field name (or path)
  2. + *
  3. Object errors only - omit 'path'
  4. + *
  5. All errors - set 'path' to '*'
  6. + *
+ * + * @author Rob Harrop + * @author Juergen Hoeller + * @author Rick Evans + * @since 2.0 + */ +public class ErrorsTag extends AbstractHtmlElementBodyTag implements BodyTag { + + /** + * The key under which this tag exposes error messages in + * the {@link PageContext#PAGE_SCOPE page context scope}. + */ + public static final String MESSAGES_ATTRIBUTE = "messages"; + + /** + * The HTML 'span' tag. + */ + public static final String SPAN_TAG = "span"; + + + private String element = SPAN_TAG; + + private String delimiter = "
"; + + /** + * Stores any value that existed in the 'errors messages' before the tag was started. + */ + private Object oldMessages; + + private boolean errorMessagesWereExposed; + + + /** + * Set the HTML element must be used to render the error messages. + *

Defaults to an HTML '<span/>' tag. + */ + public void setElement(String element) { + Assert.hasText(element, "'element' cannot be null or blank"); + this.element = element; + } + + /** + * Get the HTML element must be used to render the error messages. + */ + public String getElement() { + return this.element; + } + + /** + * Set the delimiter to be used between error messages. + *

Defaults to an HTML '<br/>' tag. + */ + public void setDelimiter(String delimiter) { + this.delimiter = delimiter; + } + + /** + * Return the delimiter to be used between error messages. + */ + public String getDelimiter() { + return this.delimiter; + } + + + /** + * Get the value for the HTML 'name' attribute. + *

Simply returns null because the 'name' attribute + * is not a validate attribute for the 'span' element. + */ + protected String getName() throws JspException { + return null; + } + + /** + * Get the value for the HTML 'id' attribute. + *

Appends '.errors' to the value returned by {@link #getPropertyPath()} + * or to the model attribute name if the <form:errors/> tag's + * 'path' attribute has been omitted. + * @return the value for the HTML 'id' attribute + * @see #getPropertyPath() + */ + protected String autogenerateId() throws JspException { + String path = getPropertyPath(); + if ("".equals(path)) { + path = (String) this.pageContext.getAttribute( + FormTag.MODEL_ATTRIBUTE_VARIABLE_NAME, PageContext.REQUEST_SCOPE); + } + return path + ".errors"; + } + + /** + * Should rendering of this tag proceed at all? + *

Only renders output when there are errors for the configured {@link #setPath path}. + * @return true only when there are errors for the configured {@link #setPath path} + */ + protected boolean shouldRender() throws JspException { + try { + return getBindStatus().isError(); + } + catch (IllegalStateException ex) { + // Neither BindingResult nor target object available. + return false; + } + } + + protected void renderDefaultContent(TagWriter tagWriter) throws JspException { + tagWriter.startTag(getElement()); + writeDefaultAttributes(tagWriter); + String delimiter = ObjectUtils.getDisplayString(evaluate("delimiter", getDelimiter())); + String[] errorMessages = getBindStatus().getErrorMessages(); + for (int i = 0; i < errorMessages.length; i++) { + String errorMessage = errorMessages[i]; + if (i > 0) { + tagWriter.appendValue(delimiter); + } + tagWriter.appendValue(getDisplayString(errorMessage)); + } + tagWriter.endTag(); + } + + /** + * Exposes any bind status error messages under {@link #MESSAGES_ATTRIBUTE this key} + * in the {@link PageContext#PAGE_SCOPE}. + *

Only called if {@link #shouldRender()} returns true. + * @see #removeAttributes() + */ + protected void exposeAttributes() throws JspException { + List errorMessages = new ArrayList(); + errorMessages.addAll(Arrays.asList(getBindStatus().getErrorMessages())); + this.oldMessages = this.pageContext.getAttribute(MESSAGES_ATTRIBUTE, PageContext.PAGE_SCOPE); + this.pageContext.setAttribute(MESSAGES_ATTRIBUTE, errorMessages, PageContext.PAGE_SCOPE); + this.errorMessagesWereExposed = true; + } + + /** + * Removes any bind status error messages that were previously stored under + * {@link #MESSAGES_ATTRIBUTE this key} in the {@link PageContext#PAGE_SCOPE}. + * @see #exposeAttributes() + */ + protected void removeAttributes() { + if (this.errorMessagesWereExposed) { + if (this.oldMessages != null) { + this.pageContext.setAttribute(MESSAGES_ATTRIBUTE, this.oldMessages, PageContext.PAGE_SCOPE); + this.oldMessages = null; + } + else { + this.pageContext.removeAttribute(MESSAGES_ATTRIBUTE, PageContext.PAGE_SCOPE); + } + } + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/FormTag.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/FormTag.java new file mode 100644 index 00000000000..6cc5c356ee4 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/FormTag.java @@ -0,0 +1,424 @@ +/* + * Copyright 2002-2008 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.web.servlet.tags.form; + +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.jsp.JspException; +import javax.servlet.jsp.PageContext; + +import org.springframework.beans.PropertyAccessor; +import org.springframework.core.Conventions; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.util.HtmlUtils; + +/** + * Databinding-aware JSP tag for rendering an HTML 'form' whose + * inner elements are bound to properties on a form object. + * + *

Users should place the form object into the + * {@link org.springframework.web.servlet.ModelAndView ModelAndView} when + * populating the data for their view. The name of this form object can be + * configured using the {@link #setModelAttribute "modelAttribute"} property. + * + *

The default value for the {@link #setModelAttribute "modelAttribute"} + * property is 'command' which corresponds to the default name + * when using the + * {@link org.springframework.web.servlet.mvc.SimpleFormController SimpleFormController}. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + * @see org.springframework.web.servlet.mvc.SimpleFormController + */ +public class FormTag extends AbstractHtmlElementTag { + + /** The default HTTP method using which form values are sent to the server: "post" */ + private static final String DEFAULT_METHOD = "post"; + + /** The default attribute name: "command" */ + public static final String DEFAULT_COMMAND_NAME = "command"; + + /** The name of the 'modelAttribute' setting */ + private static final String MODEL_ATTRIBUTE = "modelAttribute"; + + /** + * The name of the {@link javax.servlet.jsp.PageContext} attribute under which the + * form object name is exposed. + */ + public static final String MODEL_ATTRIBUTE_VARIABLE_NAME = + Conventions.getQualifiedAttributeName(AbstractFormTag.class, MODEL_ATTRIBUTE); + + + private static final String ACTION_ATTRIBUTE = "action"; + + private static final String METHOD_ATTRIBUTE = "method"; + + private static final String TARGET_ATTRIBUTE = "target"; + + private static final String ENCTYPE_ATTRIBUTE = "enctype"; + + private static final String ACCEPT_CHARSET_ATTRIBUTE = "accept-charset"; + + private static final String ONSUBMIT_ATTRIBUTE = "onsubmit"; + + private static final String ONRESET_ATTRIBUTE = "onreset"; + + private static final String AUTOCOMPLETE_ATTRIBUTE = "autocomplete"; + + + private TagWriter tagWriter; + + private String modelAttribute = DEFAULT_COMMAND_NAME; + + private String name; + + private String action; + + private String method = DEFAULT_METHOD; + + private String target; + + private String enctype; + + private String acceptCharset; + + private String onsubmit; + + private String onreset; + + private String autocomplete; + + /** Caching a previous nested path, so that it may be reset */ + private String previousNestedPath; + + + /** + * Set the name of the form attribute in the model. + *

May be a runtime expression. + */ + public void setModelAttribute(String modelAttribute) { + this.modelAttribute = modelAttribute; + } + + /** + * Get the name of the form attribute in the model. + */ + protected String getModelAttribute() { + return this.modelAttribute; + } + + /** + * Set the name of the form attribute in the model. + *

May be a runtime expression. + * @see #setModelAttribute + */ + public void setCommandName(String commandName) { + this.modelAttribute = commandName; + } + + /** + * Get the name of the form attribute in the model. + * @see #getModelAttribute + */ + protected String getCommandName() { + return this.modelAttribute; + } + + /** + * Set the value of the 'name' attribute. + *

May be a runtime expression. + *

Name is not a valid attribute for form on XHTML 1.0. However, + * it is sometimes needed for backward compatibility. + */ + public void setName(String name) { + this.name = name; + } + + /** + * Get the value of the 'name' attribute. + */ + protected String getName() throws JspException { + return this.name; + } + + /** + * Set the value of the 'action' attribute. + *

May be a runtime expression. + */ + public void setAction(String action) { + this.action = (action != null ? action : ""); + } + + /** + * Get the value of the 'action' attribute. + */ + protected String getAction() { + return this.action; + } + + /** + * Set the value of the 'method' attribute. + *

May be a runtime expression. + */ + public void setMethod(String method) { + this.method = method; + } + + /** + * Get the value of the 'method' attribute. + */ + protected String getMethod() { + return this.method; + } + + /** + * Set the value of the 'target' attribute. + *

May be a runtime expression. + */ + public void setTarget(String target) { + this.target = target; + } + + /** + * Get the value of the 'target' attribute. + */ + public String getTarget() { + return this.target; + } + + /** + * Set the value of the 'enctype' attribute. + *

May be a runtime expression. + */ + public void setEnctype(String enctype) { + this.enctype = enctype; + } + + /** + * Get the value of the 'enctype' attribute. + */ + protected String getEnctype() { + return this.enctype; + } + + /** + * Set the value of the 'acceptCharset' attribute. + *

May be a runtime expression. + */ + public void setAcceptCharset(String acceptCharset) { + this.acceptCharset = acceptCharset; + } + + /** + * Get the value of the 'acceptCharset' attribute. + */ + protected String getAcceptCharset() { + return this.acceptCharset; + } + + /** + * Set the value of the 'onsubmit' attribute. + *

May be a runtime expression. + */ + public void setOnsubmit(String onsubmit) { + this.onsubmit = onsubmit; + } + + /** + * Get the value of the 'onsubmit' attribute. + */ + protected String getOnsubmit() { + return this.onsubmit; + } + + /** + * Set the value of the 'onreset' attribute. + *

May be a runtime expression. + */ + public void setOnreset(String onreset) { + this.onreset = onreset; + } + + /** + * Get the value of the 'onreset' attribute. + */ + protected String getOnreset() { + return this.onreset; + } + + /** + * Set the value of the 'autocomplete' attribute. + * May be a runtime expression. + */ + public void setAutocomplete(String autocomplete) { + this.autocomplete = autocomplete; + } + + /** + * Get the value of the 'autocomplete' attribute. + */ + protected String getAutocomplete() { + return this.autocomplete; + } + + + + /** + * Writes the opening part of the block 'form' tag and exposes + * the form object name in the {@link javax.servlet.jsp.PageContext}. + * @param tagWriter the {@link TagWriter} to which the form content is to be written + * @return {@link javax.servlet.jsp.tagext.Tag#EVAL_BODY_INCLUDE} + */ + protected int writeTagContent(TagWriter tagWriter) throws JspException { + this.tagWriter = tagWriter; + + tagWriter.startTag("form"); + writeDefaultAttributes(tagWriter); + tagWriter.writeAttribute(ACTION_ATTRIBUTE, resolveAction()); + writeOptionalAttribute(tagWriter, METHOD_ATTRIBUTE, getMethod()); + writeOptionalAttribute(tagWriter, TARGET_ATTRIBUTE, getTarget()); + writeOptionalAttribute(tagWriter, ENCTYPE_ATTRIBUTE, getEnctype()); + writeOptionalAttribute(tagWriter, ACCEPT_CHARSET_ATTRIBUTE, getAcceptCharset()); + writeOptionalAttribute(tagWriter, ONSUBMIT_ATTRIBUTE, getOnsubmit()); + writeOptionalAttribute(tagWriter, ONRESET_ATTRIBUTE, getOnreset()); + writeOptionalAttribute(tagWriter, AUTOCOMPLETE_ATTRIBUTE, getAutocomplete()); + + tagWriter.forceBlock(); + + // Expose the form object name for nested tags... + String modelAttribute = resolveModelAttribute(); + this.pageContext.setAttribute(MODEL_ATTRIBUTE_VARIABLE_NAME, modelAttribute, PageContext.REQUEST_SCOPE); + this.pageContext.setAttribute(COMMAND_NAME_VARIABLE_NAME, modelAttribute, PageContext.REQUEST_SCOPE); + + // Save previous nestedPath value, build and expose current nestedPath value. + // Use request scope to expose nestedPath to included pages too. + this.previousNestedPath = + (String) this.pageContext.getAttribute(NESTED_PATH_VARIABLE_NAME, PageContext.REQUEST_SCOPE); + this.pageContext.setAttribute(NESTED_PATH_VARIABLE_NAME, + modelAttribute + PropertyAccessor.NESTED_PROPERTY_SEPARATOR, PageContext.REQUEST_SCOPE); + + return EVAL_BODY_INCLUDE; + } + + /** + * Autogenerated IDs correspond to the form object name. + */ + protected String autogenerateId() throws JspException { + return resolveModelAttribute(); + } + + /** + * {@link #evaluate Resolves} and returns the name of the form object. + * @throws IllegalArgumentException if the form object resolves to null + */ + protected String resolveModelAttribute() throws JspException { + Object resolvedModelAttribute = evaluate(MODEL_ATTRIBUTE, getModelAttribute()); + if (resolvedModelAttribute == null) { + throw new IllegalArgumentException(MODEL_ATTRIBUTE + " must not be null"); + } + return (String) resolvedModelAttribute; + } + + /** + * Resolve the value of the 'action' attribute. + *

If the user configured an 'action' value then + * the result of evaluating this value is used. Otherwise, the + * {@link org.springframework.web.servlet.support.RequestContext#getRequestUri() originating URI} + * is used. + * @return the value that is to be used for the 'action' attribute + */ + protected String resolveAction() throws JspException { + String action = getAction(); + if (StringUtils.hasText(action)) { + return getDisplayString(evaluate(ACTION_ATTRIBUTE, action)); + } + else { + String requestUri = getRequestContext().getRequestUri(); + ServletResponse response = this.pageContext.getResponse(); + if (response instanceof HttpServletResponse) { + requestUri = ((HttpServletResponse) response).encodeURL(requestUri); + String queryString = getRequestContext().getQueryString(); + if (StringUtils.hasText(queryString)) { + requestUri += "?" + HtmlUtils.htmlEscape(queryString); + } + } + if (StringUtils.hasText(requestUri)) { + return requestUri; + } + else { + throw new IllegalArgumentException("Attribute 'action' is required. " + + "Attempted to resolve against current request URI but request URI was null."); + } + } + } + + + /** + * Closes the 'form' block tag and removes the form object name + * from the {@link javax.servlet.jsp.PageContext}. + */ + public int doEndTag() throws JspException { + this.tagWriter.endTag(); + this.pageContext.removeAttribute(MODEL_ATTRIBUTE_VARIABLE_NAME, PageContext.REQUEST_SCOPE); + this.pageContext.removeAttribute(COMMAND_NAME_VARIABLE_NAME, PageContext.REQUEST_SCOPE); + if (this.previousNestedPath != null) { + // Expose previous nestedPath value. + this.pageContext.setAttribute(NESTED_PATH_VARIABLE_NAME, this.previousNestedPath, PageContext.REQUEST_SCOPE); + } + else { + // Remove exposed nestedPath value. + this.pageContext.removeAttribute(NESTED_PATH_VARIABLE_NAME, PageContext.REQUEST_SCOPE); + } + return EVAL_PAGE; + } + + /** + * Clears the stored {@link TagWriter}. + */ + public void doFinally() { + super.doFinally(); + this.tagWriter = null; + this.previousNestedPath = null; + } + + + /** + * Override resolve CSS class since error class is not supported. + */ + protected String resolveCssClass() throws JspException { + return ObjectUtils.getDisplayString(evaluate("cssClass", getCssClass())); + } + + /** + * Unsupported for forms. + * @throws UnsupportedOperationException always + */ + public void setPath(String path) { + throw new UnsupportedOperationException("The 'path' attribute is not supported for forms"); + } + + /** + * Unsupported for forms. + * @throws UnsupportedOperationException always + */ + public void setCssErrorClass(String cssErrorClass) { + throw new UnsupportedOperationException("The 'cssErrorClass' attribute is not supported for forms"); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/HiddenInputTag.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/HiddenInputTag.java new file mode 100644 index 00000000000..5f5b5e71efb --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/HiddenInputTag.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2008 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.web.servlet.tags.form; + +import javax.servlet.jsp.JspException; + +/** + * Data-binding aware JSP tag for rendering a hidden HTML 'input' field + * containing the databound value. + * + *

Example (binding to 'name' property of form backing object): + *