From eac621dccbf64882870a5950e973f1d18053783a Mon Sep 17 00:00:00 2001 From: Keith Donald Date: Fri, 31 Jul 2009 03:44:59 +0000 Subject: [PATCH] moved multipart and bind to web project since independent of servlet and required by new native web request parameter map git-svn-id: https://src.springframework.org/svn/spring-framework/trunk@1665 50f2f4bb-b051-0410-bef5-90022cba6387 --- org.springframework.web/.classpath | 2 + org.springframework.web/ivy.xml | 3 + org.springframework.web/pom.xml | 6 + .../web/bind/EscapedErrors.java | 211 +++++ ...ssingServletRequestParameterException.java | 63 ++ .../bind/ServletRequestBindingException.java | 51 ++ .../web/bind/ServletRequestDataBinder.java | 127 +++ ...ServletRequestParameterPropertyValues.java | 80 ++ .../web/bind/ServletRequestUtils.java | 709 +++++++++++++++++ ...sfiedServletRequestParameterException.java | 88 +++ .../web/bind/WebDataBinder.java | 290 +++++++ .../web/bind/annotation/CookieValue.java | 67 ++ .../web/bind/annotation/ExceptionHandler.java | 113 +++ .../web/bind/annotation/InitBinder.java | 61 ++ .../web/bind/annotation/Mapping.java | 35 + .../web/bind/annotation/ModelAttribute.java | 59 ++ .../web/bind/annotation/PathVariable.java | 26 + .../web/bind/annotation/RequestBody.java | 40 + .../web/bind/annotation/RequestHeader.java | 63 ++ .../web/bind/annotation/RequestMapping.java | 262 ++++++ .../web/bind/annotation/RequestMethod.java | 41 + .../web/bind/annotation/RequestParam.java | 64 ++ .../web/bind/annotation/ResponseBody.java | 39 + .../web/bind/annotation/ResponseStatus.java | 55 ++ .../bind/annotation/SessionAttributes.java | 71 ++ .../web/bind/annotation/package-info.java | 9 + .../HandlerMethodInvocationException.java | 41 + .../support/HandlerMethodInvoker.java | 745 ++++++++++++++++++ .../support/HandlerMethodResolver.java | 141 ++++ .../bind/annotation/support/package-info.java | 8 + .../web/bind/package-info.java | 8 + .../ConfigurableWebBindingInitializer.java | 135 ++++ .../support/DefaultSessionAttributeStore.java | 84 ++ .../bind/support/PresentationModelUtils.java | 65 ++ .../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-info.java | 8 + .../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 | 322 ++++++++ .../commons/CommonsMultipartFile.java | 194 +++++ .../commons/CommonsMultipartResolver.java | 195 +++++ .../web/multipart/commons/package-info.java | 9 + .../web/multipart/package-info.java | 11 + .../AbstractMultipartHttpServletRequest.java | 92 +++ .../support/ByteArrayMultipartFileEditor.java | 58 ++ .../DefaultMultipartHttpServletRequest.java | 127 +++ .../multipart/support/MultipartFilter.java | 151 ++++ .../support/StringMultipartFileEditor.java | 78 ++ .../web/multipart/support/package-info.java | 10 + .../mock/web/PassThroughFilterChain.java | 85 ++ .../web/bind/EscapedErrorsTests.java | 90 +++ .../bind/ServletRequestDataBinderTests.java | 256 ++++++ .../web/bind/ServletRequestUtilsTests.java | 465 +++++++++++ .../CommonsMultipartResolverTests.java | 485 ++++++++++++ .../ByteArrayMultipartFileEditorTests.java | 92 +++ org.springframework.web/template.mf | 3 + 65 files changed, 7405 insertions(+) create mode 100644 org.springframework.web/src/main/java/org/springframework/web/bind/EscapedErrors.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/bind/MissingServletRequestParameterException.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/bind/ServletRequestBindingException.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/bind/ServletRequestParameterPropertyValues.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/bind/ServletRequestUtils.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/bind/UnsatisfiedServletRequestParameterException.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/bind/WebDataBinder.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/bind/annotation/CookieValue.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/bind/annotation/ExceptionHandler.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/bind/annotation/InitBinder.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/bind/annotation/Mapping.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/bind/annotation/ModelAttribute.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/bind/annotation/PathVariable.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestBody.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestHeader.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestMethod.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestParam.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/bind/annotation/ResponseBody.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/bind/annotation/ResponseStatus.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/bind/annotation/SessionAttributes.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/bind/annotation/package-info.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvocationException.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodResolver.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/bind/annotation/support/package-info.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/bind/package-info.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/bind/support/ConfigurableWebBindingInitializer.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/bind/support/DefaultSessionAttributeStore.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/bind/support/PresentationModelUtils.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/bind/support/SessionAttributeStore.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/bind/support/SessionStatus.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/bind/support/SimpleSessionStatus.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/bind/support/WebArgumentResolver.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/bind/support/WebBindingInitializer.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/bind/support/package-info.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/multipart/MaxUploadSizeExceededException.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/multipart/MultipartException.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/multipart/MultipartFile.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/multipart/MultipartHttpServletRequest.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/multipart/MultipartRequest.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/multipart/MultipartResolver.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/multipart/commons/CommonsFileUploadSupport.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/multipart/commons/CommonsMultipartFile.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/multipart/commons/CommonsMultipartResolver.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/multipart/commons/package-info.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/multipart/package-info.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/multipart/support/AbstractMultipartHttpServletRequest.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/multipart/support/ByteArrayMultipartFileEditor.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/multipart/support/DefaultMultipartHttpServletRequest.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/multipart/support/MultipartFilter.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/multipart/support/StringMultipartFileEditor.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/multipart/support/package-info.java create mode 100644 org.springframework.web/src/test/java/org/springframework/mock/web/PassThroughFilterChain.java create mode 100644 org.springframework.web/src/test/java/org/springframework/web/bind/EscapedErrorsTests.java create mode 100644 org.springframework.web/src/test/java/org/springframework/web/bind/ServletRequestDataBinderTests.java create mode 100644 org.springframework.web/src/test/java/org/springframework/web/bind/ServletRequestUtilsTests.java create mode 100644 org.springframework.web/src/test/java/org/springframework/web/multipart/commons/CommonsMultipartResolverTests.java create mode 100644 org.springframework.web/src/test/java/org/springframework/web/multipart/support/ByteArrayMultipartFileEditorTests.java diff --git a/org.springframework.web/.classpath b/org.springframework.web/.classpath index 457dac5485f..f069407ef7a 100644 --- a/org.springframework.web/.classpath +++ b/org.springframework.web/.classpath @@ -20,6 +20,8 @@ + + diff --git a/org.springframework.web/ivy.xml b/org.springframework.web/ivy.xml index f629e875db8..22b23da8713 100644 --- a/org.springframework.web/ivy.xml +++ b/org.springframework.web/ivy.xml @@ -13,6 +13,7 @@ + @@ -47,6 +48,8 @@ + 1.4 true + + commons-fileupload + commons-fileupload + 1.2 + true + commons-httpclient commons-httpclient diff --git a/org.springframework.web/src/main/java/org/springframework/web/bind/EscapedErrors.java b/org.springframework.web/src/main/java/org/springframework/web/bind/EscapedErrors.java new file mode 100644 index 00000000000..03230048e93 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/bind/EscapedErrors.java @@ -0,0 +1,211 @@ +/* + * 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.util.ArrayList; +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 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); + } + + @SuppressWarnings("unchecked") + private T escapeObjectError(T 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 (T) new FieldError( + fieldError.getObjectName(), fieldError.getField(), value, + fieldError.isBindingFailure(), fieldError.getCodes(), + fieldError.getArguments(), HtmlUtils.htmlEscape(fieldError.getDefaultMessage())); + } + else { + return (T) new ObjectError( + source.getObjectName(), source.getCodes(), source.getArguments(), + HtmlUtils.htmlEscape(source.getDefaultMessage())); + } + } + + private List escapeObjectErrors(List source) { + List escaped = new ArrayList(source.size()); + for (T objectError : source) { + escaped.add(escapeObjectError(objectError)); + } + return escaped; + } + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/bind/MissingServletRequestParameterException.java b/org.springframework.web/src/main/java/org/springframework/web/bind/MissingServletRequestParameterException.java new file mode 100644 index 00000000000..fdd22f97547 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/bind/MissingServletRequestParameterException.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2009 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 final String parameterName; + + private final 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; + } + + + @Override + public String getMessage() { + return "Required " + this.parameterType + " parameter '" + this.parameterName + "' is not present"; + } + + /** + * Return the name of the offending parameter. + */ + public final String getParameterName() { + return this.parameterName; + } + + /** + * Return the expected type of the offending parameter. + */ + public final String getParameterType() { + return this.parameterType; + } + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/bind/ServletRequestBindingException.java b/org.springframework.web/src/main/java/org/springframework/web/bind/ServletRequestBindingException.java new file mode 100644 index 00000000000..8165950107e --- /dev/null +++ b/org.springframework.web/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/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java b/org.springframework.web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java new file mode 100644 index 00000000000..5bc4cd7b622 --- /dev/null +++ b/org.springframework.web/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/src/main/java/org/springframework/web/bind/ServletRequestParameterPropertyValues.java b/org.springframework.web/src/main/java/org/springframework/web/bind/ServletRequestParameterPropertyValues.java new file mode 100644 index 00000000000..a5e36bb89df --- /dev/null +++ b/org.springframework.web/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/src/main/java/org/springframework/web/bind/ServletRequestUtils.java b/org.springframework.web/src/main/java/org/springframework/web/bind/ServletRequestUtils.java new file mode 100644 index 00000000000..28e4e34228e --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/bind/ServletRequestUtils.java @@ -0,0 +1,709 @@ +/* + * 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; + +/** + * 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 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 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 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 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)); + } + + /** + * 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 T 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 T doParse(String parameter) throws NumberFormatException; + } + + + private static class IntParser extends ParameterParser { + + @Override + protected String getType() { + return "int"; + } + + @Override + protected Integer doParse(String s) throws NumberFormatException { + return Integer.valueOf(s); + } + + public int parseInt(String name, String parameter) throws ServletRequestBindingException { + return parse(name, parameter); + } + + 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 { + + @Override + protected String getType() { + return "long"; + } + + @Override + protected Long doParse(String parameter) throws NumberFormatException { + return Long.valueOf(parameter); + } + + public long parseLong(String name, String parameter) throws ServletRequestBindingException { + return parse(name, parameter); + } + + 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 { + + @Override + protected String getType() { + return "float"; + } + + @Override + protected Float doParse(String parameter) throws NumberFormatException { + return Float.valueOf(parameter); + } + + public float parseFloat(String name, String parameter) throws ServletRequestBindingException { + return parse(name, parameter); + } + + 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 { + + @Override + protected String getType() { + return "double"; + } + + @Override + protected Double doParse(String parameter) throws NumberFormatException { + return Double.valueOf(parameter); + } + + public double parseDouble(String name, String parameter) throws ServletRequestBindingException { + return parse(name, parameter); + } + + 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 { + + @Override + protected String getType() { + return "boolean"; + } + + @Override + protected Boolean doParse(String parameter) throws NumberFormatException { + return (parameter.equalsIgnoreCase("true") || parameter.equalsIgnoreCase("on") || + parameter.equalsIgnoreCase("yes") || parameter.equals("1")); + } + + public boolean parseBoolean(String name, String parameter) throws ServletRequestBindingException { + return parse(name, parameter); + } + + 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 { + + @Override + protected String getType() { + return "string"; + } + + @Override + protected String 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 (String value : values) { + validateRequiredParameter(name, value); + } + return values; + } + } + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/bind/UnsatisfiedServletRequestParameterException.java b/org.springframework.web/src/main/java/org/springframework/web/bind/UnsatisfiedServletRequestParameterException.java new file mode 100644 index 00000000000..8cd7d019bca --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/bind/UnsatisfiedServletRequestParameterException.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2009 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.Iterator; +import java.util.Map; + +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * {@link ServletRequestBindingException} subclass that indicates an unsatisfied + * parameter condition, as typically expressed using an @RequestMapping + * annotation at the @Controller type level. + * + * @author Juergen Hoeller + * @since 3.0 + * @see org.springframework.web.bind.annotation.RequestMapping#params() + */ +public class UnsatisfiedServletRequestParameterException extends ServletRequestBindingException { + + private final String[] paramConditions; + + private final Map actualParams; + + + /** + * Create a new UnsatisfiedServletRequestParameterException. + * @param paramConditions the parameter conditions that have been violated + * @param actualParams the actual parameter Map associated with the ServletRequest + */ + @SuppressWarnings("unchecked") + public UnsatisfiedServletRequestParameterException(String[] paramConditions, Map actualParams) { + super(""); + this.paramConditions = paramConditions; + this.actualParams = (Map) actualParams; + } + + + @Override + public String getMessage() { + return "Parameter conditions \"" + StringUtils.arrayToDelimitedString(this.paramConditions, ", ") + + "\" not met for actual request parameters: " + requestParameterMapToString(this.actualParams); + } + + private static String requestParameterMapToString(Map actualParams) { + StringBuilder result = new StringBuilder(); + for (Iterator> it = actualParams.entrySet().iterator(); it.hasNext();) { + Map.Entry entry = it.next(); + result.append(entry.getKey()).append('=').append(ObjectUtils.nullSafeToString(entry.getValue())); + if (it.hasNext()) { + result.append(", "); + } + } + return result.toString(); + } + + /** + * Return the parameter conditions that have been violated. + * @see org.springframework.web.bind.annotation.RequestMapping#params() + */ + public final String[] getParamConditions() { + return this.paramConditions; + } + + /** + * Return the actual parameter Map associated with the ServletRequest. + * @see javax.servlet.ServletRequest#getParameterMap() + */ + public final Map getActualParams() { + return this.actualParams; + } + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/bind/WebDataBinder.java b/org.springframework.web/src/main/java/org/springframework/web/bind/WebDataBinder.java new file mode 100644 index 00000000000..f4359bd46d5 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/bind/WebDataBinder.java @@ -0,0 +1,290 @@ +/* + * 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.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. Default values, for parameters that are otherwise + * not present, can specify a value for the field other then empty. + * + * @author Juergen Hoeller + * @author Scott Andrews + * @since 1.2 + * @see #registerCustomEditor + * @see #setAllowedFields + * @see #setRequiredFields + * @see #setFieldMarkerPrefix + * @see #setFieldDefaultPrefix + * @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 = "_"; + + /** + * Default prefix that field default parameters start with, followed by the field + * name: e.g. "!subscribeToNewsletter" for a field "subscribeToNewsletter". + *

Default parameters differ from field markers in that they provide a default + * value instead of an empty value. + * @see #setFieldDefaultPrefix + */ + public static final String DEFAULT_FIELD_DEFAULT_PREFIX = "!"; + + private String fieldMarkerPrefix = DEFAULT_FIELD_MARKER_PREFIX; + + private String fieldDefaultPrefix = DEFAULT_FIELD_DEFAULT_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; + } + + /** + * Specify a prefix that can be used for parameters that indicate default + * value fields, having "prefix + field" as name. The value of the default + * field is used when the field is not provided. + *

Default is "!", for "!FIELD" parameters (e.g. "!subscribeToNewsletter"). + * Set this to null if you want to turn off the field defaults 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. A default field is especially + * useful when a checkbox represents a non-boolean value. + *

The presence of a default parameter preempts the behavior of a field + * marker for the given field. + * @see #DEFAULT_FIELD_DEFAULT_PREFIX + * @see org.springframework.web.servlet.mvc.BaseCommandController#onBind + */ + public void setFieldDefaultPrefix(String fieldDefaultPrefix) { + this.fieldDefaultPrefix = fieldDefaultPrefix; + } + + /** + * Return the prefix for parameters that mark default fields. + */ + public String getFieldDefaultPrefix() { + return this.fieldDefaultPrefix; + } + + /** + * 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 default and marker check + * before delegating to the superclass binding process. + * @see #checkFieldDefaults + * @see #checkFieldMarkers + */ + @Override + protected void doBind(MutablePropertyValues mpvs) { + checkFieldDefaults(mpvs); + checkFieldMarkers(mpvs); + super.doBind(mpvs); + } + + /** + * Check the given property values for field defaults, + * i.e. for fields that start with the field default prefix. + *

The existence of a field defaults indicates that the specified + * value should be used if the field is otherwise not present. + * @param mpvs the property values to be bound (can be modified) + * @see #getFieldDefaultPrefix + */ + protected void checkFieldDefaults(MutablePropertyValues mpvs) { + if (getFieldDefaultPrefix() != null) { + String fieldDefaultPrefix = getFieldDefaultPrefix(); + PropertyValue[] pvArray = mpvs.getPropertyValues(); + for (PropertyValue pv : pvArray) { + if (pv.getName().startsWith(fieldDefaultPrefix)) { + String field = pv.getName().substring(fieldDefaultPrefix.length()); + if (getPropertyAccessor().isWritableProperty(field) && !mpvs.contains(field)) { + mpvs.addPropertyValue(field, pv.getValue()); + } + mpvs.removePropertyValue(pv); + } + } + } + } + + /** + * 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 (PropertyValue pv : pvArray) { + 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 (Map.Entry entry : multipartFiles.entrySet()) { + String key = entry.getKey(); + MultipartFile value = entry.getValue(); + if (isBindEmptyMultipartFiles() || !value.isEmpty()) { + mpvs.addPropertyValue(key, value); + } + } + } + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/CookieValue.java b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/CookieValue.java new file mode 100644 index 00000000000..281c5c2072c --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/CookieValue.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2009 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 an HTTP cookie. + * Supported for annotated handler methods in Servlet and Portlet environments. + * + *

The method parameter may be declared as type {@link javax.servlet.http.Cookie} + * or as cookie value type (String, int, etc). + * + * @author Juergen Hoeller + * @since 3.0 + * @see RequestMapping + * @see RequestParam + * @see RequestHeader + * @see org.springframework.web.bind.annotation.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 CookieValue { + + /** + * The name of the cookie to bind to. + */ + String value() default ""; + + /** + * Whether the header is required. + *

Default is true, leading to an exception thrown in case + * of the header missing in the request. Switch this to false + * if you prefer a null in case of the header missing. + *

Alternatively, provide a {@link #defaultValue() defaultValue}, + * which implicitely sets this flag to false. + */ + boolean required() default true; + + /** + * The default value to use as a fallback. Supplying a default value implicitely + * sets {@link #required()} to false. + */ + String defaultValue() default ""; + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/ExceptionHandler.java b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/ExceptionHandler.java new file mode 100644 index 00000000000..69586992c2a --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/ExceptionHandler.java @@ -0,0 +1,113 @@ +/* + * Copyright 2002-2009 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 handling exceptions in specific handler classes and/or + * handler methods. Provides consistent style between Servlet and Portlet + * environments, with the semantics adapting to the concrete environment. + * + *

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: + *

    + *
  • Request and/or response objects (Servlet API or Portlet API). + * You may choose any specific request/response type, e.g. + * {@link javax.servlet.ServletRequest} / {@link javax.servlet.http.HttpServletRequest} + * or {@link javax.portlet.PortletRequest} / {@link javax.portlet.ActionRequest} / + * {@link javax.portlet.RenderRequest}. Note that in the Portlet case, + * an explicitly declared action/render argument is also used for mapping + * specific request types onto a handler method (in case of no other + * information given that differentiates between action and render requests). + *
  • Session object (Servlet API or Portlet API): either + * {@link javax.servlet.http.HttpSession} or {@link javax.portlet.PortletSession}. + * An argument of this type will enforce the presence of a corresponding session. + * As a consequence, such an argument will never be null. + * Note that session access may not be thread-safe, in particular in a + * Servlet environment: Consider switching the + * {@link org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter#setSynchronizeOnSession "synchronizeOnSession"} + * flag to "true" if multiple requests are allowed to access a session concurrently. + *
  • {@link org.springframework.web.context.request.WebRequest} or + * {@link org.springframework.web.context.request.NativeWebRequest}. + * Allows for generic request parameter access as well as request/session + * attribute access, without ties to the native Servlet/Portlet API. + *
  • {@link java.util.Locale} for the current request locale + * (determined by the most specific locale resolver available, + * i.e. the configured {@link org.springframework.web.servlet.LocaleResolver} + * in a Servlet environment and the portal locale in a Portlet environment). + *
  • {@link java.io.InputStream} / {@link java.io.Reader} for access + * to the request's content. This will be the raw InputStream/Reader as + * exposed by the Servlet/Portlet API. + *
  • {@link java.io.OutputStream} / {@link java.io.Writer} for generating + * the response's content. This will be the raw OutputStream/Writer as + * exposed by the Servlet/Portlet API. + *
+ * + *

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

    + *
  • A ModelAndView object (Servlet MVC or Portlet MVC). + *
  • A {@link org.springframework.ui.Model Model} object, with the view name + * implicitly determined through a {@link org.springframework.web.servlet.RequestToViewNameTranslator}. + *
  • A {@link java.util.Map} object for exposing a model, + * with the view name implicitly determined through a + * {@link org.springframework.web.servlet.RequestToViewNameTranslator}. + *
  • A {@link org.springframework.web.servlet.View} object. + *
  • A {@link java.lang.String} value which is interpreted as view name. + *
  • void if the method handles the response itself (by + * writing the response content directly, declaring an argument of type + * {@link javax.servlet.ServletResponse} / {@link javax.servlet.http.HttpServletResponse} + * / {@link javax.portlet.RenderResponse} for that purpose) + * or if the view name is supposed to be implicitly determined through a + * {@link org.springframework.web.servlet.RequestToViewNameTranslator} + * (not declaring a response argument in the handler method signature; + * only applicable in a Servlet environment). + *
+ * + *

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 Arjen Poutsma + * @see org.springframework.web.context.request.WebRequest + * @see org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver + * @since 3.0 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ExceptionHandler { + + /** + * Exceptions handled by the annotation method. If empty, will default to any exceptions listed in the method + * argument list. + */ + Class[] value() default {}; + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/InitBinder.java b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/InitBinder.java new file mode 100644 index 00000000000..9b22b261c4f --- /dev/null +++ b/org.springframework.web/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/src/main/java/org/springframework/web/bind/annotation/Mapping.java b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/Mapping.java new file mode 100644 index 00000000000..afbd1f2e723 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/Mapping.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2009 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.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Meta annotation that indicates a web mapping annotation. + * + * @author Juergen Hoeller + * @since 3.0 + * @see RequestMapping + */ +@Target({ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Mapping { + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/ModelAttribute.java b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/ModelAttribute.java new file mode 100644 index 00000000000..dbd293ff867 --- /dev/null +++ b/org.springframework.web/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/src/main/java/org/springframework/web/bind/annotation/PathVariable.java b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/PathVariable.java new file mode 100644 index 00000000000..9fff5eff2e2 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/PathVariable.java @@ -0,0 +1,26 @@ +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 URI template variable. Supported for {@link + * RequestMapping} annotated handler methods in Servlet environments. + * + * @author Arjen Poutsma + * @see RequestMapping + * @see org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter + * @since 3.0 + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface PathVariable { + + /** The URI template variable to bind to. */ + String value() default ""; + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestBody.java b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestBody.java new file mode 100644 index 00000000000..ebf36beab64 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestBody.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2009 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 the web request body. Supported for annotated + * handler methods in Servlet environments. + * + * @author Arjen Poutsma + * @see RequestHeader + * @see ResponseBody + * @see org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter + * @since 3.0 + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RequestBody { + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestHeader.java b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestHeader.java new file mode 100644 index 00000000000..e9214a2de43 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestHeader.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2009 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 header. + * Supported for annotated handler methods in Servlet and Portlet environments. + * + * @author Juergen Hoeller + * @since 3.0 + * @see RequestMapping + * @see RequestParam + * @see CookieValue + * @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 RequestHeader { + + /** + * The name of the request header to bind to. + */ + String value() default ""; + + /** + * Whether the header is required. + *

Default is true, leading to an exception thrown in case + * of the header missing in the request. Switch this to false + * if you prefer a null in case of the header missing. + *

Alternatively, provide a {@link #defaultValue() defaultValue}, + * which implicitely sets this flag to false. + */ + boolean required() default true; + + /** + * The default value to use as a fallback. Supplying a default value implicitely + * sets {@link #required()} to false. + */ + String defaultValue() default ""; + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java new file mode 100644 index 00000000000..55db587c9b3 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java @@ -0,0 +1,262 @@ +/* + * Copyright 2002-2009 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): + *

    + *
  • Request and/or response objects (Servlet API or Portlet API). + * You may choose any specific request/response type, e.g. + * {@link javax.servlet.ServletRequest} / {@link javax.servlet.http.HttpServletRequest} + * or {@link javax.portlet.PortletRequest} / {@link javax.portlet.ActionRequest} / + * {@link javax.portlet.RenderRequest}. Note that in the Portlet case, + * an explicitly declared action/render argument is also used for mapping + * specific request types onto a handler method (in case of no other + * information given that differentiates between action and render requests). + *
  • Session object (Servlet API or Portlet API): either + * {@link javax.servlet.http.HttpSession} or {@link javax.portlet.PortletSession}. + * An argument of this type will enforce the presence of a corresponding session. + * As a consequence, such an argument will never be null. + * Note that session access may not be thread-safe, in particular in a + * Servlet environment: Consider switching the + * {@link org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter#setSynchronizeOnSession "synchronizeOnSession"} + * flag to "true" if multiple requests are allowed to access a session concurrently. + *
  • {@link org.springframework.web.context.request.WebRequest} or + * {@link org.springframework.web.context.request.NativeWebRequest}. + * Allows for generic request parameter access as well as request/session + * attribute access, without ties to the native Servlet/Portlet API. + *
  • {@link java.util.Locale} for the current request locale + * (determined by the most specific locale resolver available, + * i.e. the configured {@link org.springframework.web.servlet.LocaleResolver} + * in a Servlet environment and the portal locale in a Portlet environment). + *
  • {@link java.io.InputStream} / {@link java.io.Reader} for access + * to the request's content. This will be the raw InputStream/Reader as + * exposed by the Servlet/Portlet API. + *
  • {@link java.io.OutputStream} / {@link java.io.Writer} for generating + * the response's content. This will be the raw OutputStream/Writer as + * exposed by the Servlet/Portlet API. + *
  • {@link PathVariable @PathVariable} annotated parameters for access to + * URI template values (i.e. /hotels/{hotel}). Variable values will be + * converted to the declared method argument type. + *
  • {@link RequestParam @RequestParam} annotated parameters for access to + * specific Servlet/Portlet request parameters. Parameter values will be + * converted to the declared method argument type. + *
  • {@link RequestHeader @RequestHeader} annotated parameters for access to + * specific Servlet/Portlet request HTTP headers. Parameter values will be + * converted to the declared method argument type. + *
  • {@link RequestBody @RequestBody} annotated parameters for access to + * the Servlet request HTTP contents. Parameter values will be + * converted to the declared method argument type using + * {@linkplain org.springframework.http.converter.HttpMessageConverter message + * converters}. + *
  • {@link java.util.Map} / {@link org.springframework.ui.Model} / + * {@link org.springframework.ui.ModelMap} for enriching the implicit model + * that will be exposed to the web view. + *
  • Command/form objects to bind parameters to: as bean properties or fields, + * with customizable type conversion, depending on {@link InitBinder} methods + * and/or the HandlerAdapter configuration - see the "webBindingInitializer" + * property on AnnotationMethodHandlerAdapter. + * Such command objects along with their validation results will be exposed + * as model attributes, by default using the non-qualified command class name + * in property notation (e.g. "orderAddress" for type "mypackage.OrderAddress"). + * Specify a parameter-level {@link ModelAttribute} annotation for declaring + * a specific model attribute name. + *
  • {@link org.springframework.validation.Errors} / + * {@link org.springframework.validation.BindingResult} validation results + * for a preceding command/form object (the immediate preceding argument). + *
  • {@link org.springframework.web.bind.support.SessionStatus} status handle + * for marking form processing as complete (triggering the cleanup of session + * attributes that have been indicated by the {@link SessionAttributes} annotation + * at the handler type level). + *
+ * + *

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

    + *
  • A ModelAndView object (Servlet MVC or Portlet MVC), + * with the model implicitly enriched with command objects and the results + * of {@link ModelAttribute} annotated reference data accessor methods. + *
  • A {@link org.springframework.ui.Model Model} object, with the view name + * implicitly determined through a {@link org.springframework.web.servlet.RequestToViewNameTranslator} + * and the model implicitly enriched with command objects and the results + * of {@link ModelAttribute} annotated reference data accessor methods. + *
  • A {@link java.util.Map} object for exposing a model, + * with the view name implicitly determined through a + * {@link org.springframework.web.servlet.RequestToViewNameTranslator} + * and the model implicitly enriched with command objects and the results + * of {@link ModelAttribute} annotated reference data accessor methods. + *
  • A {@link org.springframework.web.servlet.View} object, with the + * model implicitly determined through command objects and + * {@link ModelAttribute} annotated reference data accessor methods. + * The handler method may also programmatically enrich the model by + * declaring a {@link org.springframework.ui.Model} argument (see above). + *
  • A {@link java.lang.String} value which is interpreted as view name, + * with the model implicitly determined through command objects and + * {@link ModelAttribute} annotated reference data accessor methods. + * The handler method may also programmatically enrich the model by + * declaring a {@link org.springframework.ui.ModelMap} argument + * (see above). + *
  • void if the method handles the response itself (by + * writing the response content directly, declaring an argument of type + * {@link javax.servlet.ServletResponse} / {@link javax.servlet.http.HttpServletResponse} + * / {@link javax.portlet.RenderResponse} for that purpose) + * or if the view name is supposed to be implicitly determined through a + * {@link org.springframework.web.servlet.RequestToViewNameTranslator} + * (not declaring a response argument in the handler method signature; + * only applicable in a Servlet environment). + *
  • Any other return type will be considered as single model attribute + * to be exposed to the view, using the attribute name specified through + * {@link ModelAttribute} at the method level (or the default attribute name + * based on the return type's class name otherwise). The model will be + * implicitly enriched with command objects and the results of + * {@link ModelAttribute} annotated reference data accessor 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 +@Mapping +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). + *

Supported for Servlet environments as well as Portlet 2.0 environments. + */ + 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 {}; + + /** + * The headers of the mapped request, narrowing the primary mapping. + *

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

Also supports media type wildcards (*), for headers such as Accept + * and Content-Type. For instance, + *

+	 * @RequestMapping(value = "/something", headers = "content-type=text/*")
+	 * 
+ * will match requests with a Content-Type of "text/html", "text/plain", etc. + *

Supported at the type level as well as at the method level! + * When used at the type level, all method-level mappings inherit + * this header restriction (i.e. the type-level restriction + * gets checked before the handler method is even resolved). + * @see org.springframework.http.MediaType + */ + String[] headers() default {}; +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestMethod.java b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestMethod.java new file mode 100644 index 00000000000..3695cb34251 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestMethod.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2009 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/src/main/java/org/springframework/web/bind/annotation/RequestParam.java b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestParam.java new file mode 100644 index 00000000000..acfb1939527 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestParam.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2009 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 annotated handler methods in Servlet and Portlet environments. + * + * @author Arjen Poutsma + * @author Juergen Hoeller + * @since 2.5 + * @see RequestMapping + * @see RequestHeader + * @see CookieValue + * @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 name of 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. + *

Alternatively, provide a {@link #defaultValue() defaultValue}, + * which implicitely sets this flag to false. + */ + boolean required() default true; + + /** + * The default value to use as a fallback. Supplying a default value implicitely + * sets {@link #required()} to false. + */ + String defaultValue() default ""; + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/ResponseBody.java b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/ResponseBody.java new file mode 100644 index 00000000000..1f274735113 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/ResponseBody.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2009 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 return value should be bound to the web response body. Supported for annotated + * handler methods in Servlet environments. + * + * @author Arjen Poutsma + * @see RequestBody + * @see org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter + * @since 3.0 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ResponseBody { + +} \ No newline at end of file diff --git a/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/ResponseStatus.java b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/ResponseStatus.java new file mode 100644 index 00000000000..d07c65d8a86 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/ResponseStatus.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2009 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; + +import org.springframework.http.HttpStatus; + +/** + * Marks a method or exception class with the status code and reason that should be returned. The status code is applied + * to the HTTP response when the handler method is invoked, or whenever said exception is thrown. + * + * @author Arjen Poutsma + * @see org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver + * @since 3.0 + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ResponseStatus { + + /** + * The status code to use for the response. + * + * @see javax.servlet.http.HttpServletResponse#setStatus(int) + */ + HttpStatus value(); + + /** + * The reason to be used for the response.

If this element is not set, it will default to the standard status + * message for the status code. + * + * @see javax.servlet.http.HttpServletResponse#sendError(int, String) + */ + String reason() default ""; + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/SessionAttributes.java b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/SessionAttributes.java new file mode 100644 index 00000000000..076bec779be --- /dev/null +++ b/org.springframework.web/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/src/main/java/org/springframework/web/bind/annotation/package-info.java b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/package-info.java new file mode 100644 index 00000000000..afe5c6fd6e7 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/package-info.java @@ -0,0 +1,9 @@ + +/** + * + * Annotations for binding requests to controllers and handler methods + * as well as for binding request parameters to method arguments. + * + */ +package org.springframework.web.bind.annotation; + diff --git a/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvocationException.java b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvocationException.java new file mode 100644 index 00000000000..bfbb1ac5303 --- /dev/null +++ b/org.springframework.web/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/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java new file mode 100644 index 00000000000..341db9029e2 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java @@ -0,0 +1,745 @@ +/* + * Copyright 2002-2009 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.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +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.http.HttpInputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.model.ui.PresentationModelFactory; +import org.springframework.model.ui.config.BindingLifecycle; +import org.springframework.ui.ExtendedModelMap; +import org.springframework.ui.Model; +import org.springframework.ui.MvcBindingLifecycle; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; +import org.springframework.validation.BindingResult; +import org.springframework.validation.Errors; +import org.springframework.web.HttpMediaTypeNotSupportedException; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.bind.annotation.InitBinder; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.support.DefaultSessionAttributeStore; +import org.springframework.web.bind.support.PresentationModelUtils; +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.NativeWebRequestParameterMap; +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 + * @author Arjen Poutsma + * @see #invokeHandlerMethod + * @since 2.5.2 + */ +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(); + + private final HttpMessageConverter[] messageConverters; + + + public HandlerMethodInvoker(HandlerMethodResolver methodResolver) { + this(methodResolver, null); + } + + public HandlerMethodInvoker(HandlerMethodResolver methodResolver, WebBindingInitializer bindingInitializer) { + this(methodResolver, bindingInitializer, new DefaultSessionAttributeStore(), null, new WebArgumentResolver[0], + new HttpMessageConverter[0]); + } + + public HandlerMethodInvoker(HandlerMethodResolver methodResolver, WebBindingInitializer bindingInitializer, + SessionAttributeStore sessionAttributeStore, ParameterNameDiscoverer parameterNameDiscoverer, + WebArgumentResolver[] customArgumentResolvers, HttpMessageConverter[] messageConverters) { + + this.methodResolver = methodResolver; + this.bindingInitializer = bindingInitializer; + this.sessionAttributeStore = sessionAttributeStore; + this.parameterNameDiscoverer = parameterNameDiscoverer; + this.customArgumentResolvers = customArgumentResolvers; + this.messageConverters = messageConverters; + } + + + 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; + String headerName = null; + boolean requestBodyFound = false; + String cookieName = null; + String pathVarName = null; + String attrName = null; + boolean required = false; + String defaultValue = null; + int found = 0; + Annotation[] paramAnns = methodParam.getParameterAnnotations(); + + for (Annotation paramAnn : paramAnns) { + if (RequestParam.class.isInstance(paramAnn)) { + RequestParam requestParam = (RequestParam) paramAnn; + paramName = requestParam.value(); + required = requestParam.required(); + defaultValue = requestParam.defaultValue(); + found++; + } + else if (RequestHeader.class.isInstance(paramAnn)) { + RequestHeader requestHeader = (RequestHeader) paramAnn; + headerName = requestHeader.value(); + required = requestHeader.required(); + defaultValue = requestHeader.defaultValue(); + found++; + } + else if (RequestBody.class.isInstance(paramAnn)) { + requestBodyFound = true; + found++; + } + else if (CookieValue.class.isInstance(paramAnn)) { + CookieValue cookieValue = (CookieValue) paramAnn; + cookieName = cookieValue.value(); + required = cookieValue.required(); + defaultValue = cookieValue.defaultValue(); + found++; + } + else if (PathVariable.class.isInstance(paramAnn)) { + PathVariable pathVar = (PathVariable) paramAnn; + pathVarName = pathVar.value(); + found++; + } + else if (ModelAttribute.class.isInstance(paramAnn)) { + ModelAttribute attr = (ModelAttribute) paramAnn; + attrName = attr.value(); + found++; + } + } + + if (found > 1) { + throw new IllegalStateException("Handler parameter annotations are exclusive choices - " + + "do not specify more than one such annotation on the same parameter: " + handlerMethod); + } + + if (found == 0) { + Object argValue = resolveCommonArgument(methodParam, webRequest); + if (argValue != WebArgumentResolver.UNRESOLVED) { + args[i] = argValue; + } + else { + Class paramType = methodParam.getParameterType(); + 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!"); + } + // TODO - Code Review - NEW BINDING LIFECYCLE RESOLVABLE ARG + else if (BindingLifecycle.class.isAssignableFrom(paramType)) { + Class modelType = resolveBindingLifecycleModelType(methodParam); + PresentationModelFactory factory = PresentationModelUtils.getPresentationModelFactory(webRequest); + Map fieldValues = new NativeWebRequestParameterMap(webRequest); + args[i] = new MvcBindingLifecycle(modelType, factory, implicitModel, fieldValues); + } + else if (BeanUtils.isSimpleProperty(paramType)) { + paramName = ""; + } + else { + attrName = ""; + } + } + } + + if (paramName != null) { + args[i] = resolveRequestParam(paramName, required, defaultValue, methodParam, webRequest, handler); + } + else if (headerName != null) { + args[i] = resolveRequestHeader(headerName, required, defaultValue, methodParam, webRequest, handler); + } + else if (requestBodyFound) { + args[i] = resolveRequestBody(methodParam, webRequest, handler); + } + else if (cookieName != null) { + args[i] = resolveCookieValue(cookieName, required, defaultValue, methodParam, webRequest, handler); + } + else if (pathVarName != null) { + args[i] = resolvePathVariable(pathVarName, 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; + } + + protected 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; + String paramDefaultValue = null; + String pathVarName = null; + Annotation[] paramAnns = methodParam.getParameterAnnotations(); + + for (Annotation paramAnn : paramAnns) { + if (RequestParam.class.isInstance(paramAnn)) { + RequestParam requestParam = (RequestParam) paramAnn; + paramName = requestParam.value(); + paramRequired = requestParam.required(); + paramDefaultValue = requestParam.defaultValue(); + break; + } + else if (ModelAttribute.class.isInstance(paramAnn)) { + throw new IllegalStateException( + "@ModelAttribute is not supported on @InitBinder methods: " + initBinderMethod); + } + else if (PathVariable.class.isInstance(paramAnn)) { + PathVariable pathVar = (PathVariable) paramAnn; + pathVarName = pathVar.value(); + } + } + + if (paramName == null && pathVarName == 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, paramDefaultValue, methodParam, webRequest, null); + } + else if (pathVarName != null) { + initBinderArgs[i] = resolvePathVariable(pathVarName, methodParam, webRequest, null); + } + } + + return initBinderArgs; + } + + private Object resolveRequestParam(String paramName, boolean required, String defaultValue, + MethodParameter methodParam, NativeWebRequest webRequest, Object handlerForInitBinderCall) + throws Exception { + + Class paramType = methodParam.getParameterType(); + if (paramName.length() == 0) { + paramName = getRequiredParameterName(methodParam); + } + 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 (StringUtils.hasText(defaultValue)) { + paramValue = defaultValue; + } + else if (required) { + raiseMissingParameterException(paramName, paramType); + } + paramValue = checkValue(paramName, paramValue, paramType); + } + WebDataBinder binder = createBinder(webRequest, null, paramName); + initBinder(handlerForInitBinderCall, paramName, binder, webRequest); + return binder.convertIfNecessary(paramValue, paramType, methodParam); + } + + private Object resolveRequestHeader(String headerName, boolean required, String defaultValue, + MethodParameter methodParam, NativeWebRequest webRequest, Object handlerForInitBinderCall) + throws Exception { + + Class paramType = methodParam.getParameterType(); + if (headerName.length() == 0) { + headerName = getRequiredParameterName(methodParam); + } + Object headerValue = null; + String[] headerValues = webRequest.getHeaderValues(headerName); + if (headerValues != null) { + headerValue = (headerValues.length == 1 ? headerValues[0] : headerValues); + } + if (headerValue == null) { + if (StringUtils.hasText(defaultValue)) { + headerValue = defaultValue; + } + else if (required) { + raiseMissingHeaderException(headerName, paramType); + } + headerValue = checkValue(headerName, headerValue, paramType); + } + WebDataBinder binder = createBinder(webRequest, null, headerName); + initBinder(handlerForInitBinderCall, headerName, binder, webRequest); + return binder.convertIfNecessary(headerValue, paramType, methodParam); + } + + /** + * Resolves the given {@link RequestBody @RequestBody} annotation. + */ + @SuppressWarnings("unchecked") + protected Object resolveRequestBody(MethodParameter methodParam, NativeWebRequest webRequest, Object handler) + throws Exception { + + HttpInputMessage inputMessage = createHttpInputMessage(webRequest); + Class paramType = methodParam.getParameterType(); + MediaType contentType = inputMessage.getHeaders().getContentType(); + if (contentType == null) { + StringBuilder builder = new StringBuilder(ClassUtils.getShortName(methodParam.getParameterType())); + String paramName = methodParam.getParameterName(); + if (paramName != null) { + builder.append(' '); + builder.append(paramName); + } + throw new HttpMediaTypeNotSupportedException( + "Cannot extract @RequestBody parameter (" + builder.toString() + "): no Content-Type found"); + } + List allSupportedMediaTypes = new ArrayList(); + for (HttpMessageConverter messageConverter : messageConverters) { + allSupportedMediaTypes.addAll(messageConverter.getSupportedMediaTypes()); + if (messageConverter.supports(paramType)) { + for (MediaType supportedMediaType : messageConverter.getSupportedMediaTypes()) { + if (supportedMediaType.includes(contentType)) { + return messageConverter.read(paramType, inputMessage); + } + } + } + } + throw new HttpMediaTypeNotSupportedException(contentType, allSupportedMediaTypes); + } + + /** + * Return a {@link HttpInputMessage} for the given {@link NativeWebRequest}. + * Throws an UnsupportedOperationException by default. + */ + protected HttpInputMessage createHttpInputMessage(NativeWebRequest webRequest) throws Exception { + throw new UnsupportedOperationException("@RequestBody not supported"); + } + + private Object resolveCookieValue(String cookieName, boolean required, String defaultValue, + MethodParameter methodParam, NativeWebRequest webRequest, Object handlerForInitBinderCall) + throws Exception { + + Class paramType = methodParam.getParameterType(); + if (cookieName.length() == 0) { + cookieName = getRequiredParameterName(methodParam); + } + Object cookieValue = resolveCookieValue(cookieName, paramType, webRequest); + if (cookieValue == null) { + if (StringUtils.hasText(defaultValue)) { + cookieValue = defaultValue; + } + else if (required) { + raiseMissingCookieException(cookieName, paramType); + } + cookieValue = checkValue(cookieName, cookieValue, paramType); + } + WebDataBinder binder = createBinder(webRequest, null, cookieName); + initBinder(handlerForInitBinderCall, cookieName, binder, webRequest); + return binder.convertIfNecessary(cookieValue, paramType, methodParam); + } + + /** + * Resolves the given {@link CookieValue @CookieValue} annotation. + * Throws an UnsupportedOperationException by default. + */ + protected Object resolveCookieValue(String cookieName, Class paramType, NativeWebRequest webRequest) + throws Exception { + + throw new UnsupportedOperationException("@CookieValue not supported"); + } + + private Object resolvePathVariable(String pathVarName, MethodParameter methodParam, + NativeWebRequest webRequest, Object handlerForInitBinderCall) throws Exception { + + Class paramType = methodParam.getParameterType(); + if (pathVarName.length() == 0) { + pathVarName = getRequiredParameterName(methodParam); + } + String pathVarValue = resolvePathVariable(pathVarName, paramType, webRequest); + WebDataBinder binder = createBinder(webRequest, null, pathVarName); + initBinder(handlerForInitBinderCall, pathVarName, binder, webRequest); + return binder.convertIfNecessary(pathVarValue, paramType, methodParam); + } + + /** + * Resolves the given {@link PathVariable @PathVariable} annotation. + * Throws an UnsupportedOperationException by default. + */ + protected String resolvePathVariable(String pathVarName, Class paramType, NativeWebRequest webRequest) + throws Exception { + + throw new UnsupportedOperationException("@PathVariable not supported"); + } + + private String getRequiredParameterName(MethodParameter methodParam) { + String name = methodParam.getParameterName(); + if (name == null) { + throw new IllegalStateException("No parameter name specified for argument of type [" + + methodParam.getParameterType().getName() + + "], and no parameter name information found in class file either."); + } + return name; + } + + private Object checkValue(String name, Object value, Class paramType) { + if (value == null) { + if (boolean.class.equals(paramType)) { + return Boolean.FALSE; + } + else if (paramType.isPrimitive()) { + throw new IllegalStateException("Optional " + paramType + " parameter '" + name + + "' 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."); + } + } + return value; + } + + 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 raiseMissingHeaderException(String headerName, Class paramType) throws Exception { + throw new IllegalStateException("Missing header '" + headerName + "' of type [" + paramType.getName() + "]"); + } + + protected void raiseMissingCookieException(String cookieName, Class paramType) throws Exception { + throw new IllegalStateException("Missing cookie value '" + cookieName + "' 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; + } + + protected final void addReturnValueAsModelAttribute( + Method handlerMethod, Class handlerType, Object returnValue, ExtendedModelMap implicitModel) { + + ModelAttribute attr = AnnotationUtils.findAnnotation(handlerMethod, ModelAttribute.class); + String attrName = (attr != null ? attr.value() : ""); + if ("".equals(attrName)) { + Class resolvedType = GenericTypeResolver.resolveReturnType(handlerMethod, handlerType); + attrName = Conventions.getVariableNameForReturnType(handlerMethod, resolvedType, returnValue); + } + implicitModel.addAttribute(attrName, returnValue); + } + + // TODO - Code Review - BINDING LIFECYCLE RELATED INTERNAL HELPERS + + // TODO - this generic arg identification looping code is duplicated in several places now... + private Class resolveBindingLifecycleModelType(MethodParameter methodParam) { + Type type = GenericTypeResolver.getTargetType(methodParam); + if (type instanceof ParameterizedType) { + ParameterizedType paramType = (ParameterizedType) type; + Type rawType = paramType.getRawType(); + Type arg = paramType.getActualTypeArguments()[0]; + if (arg instanceof TypeVariable) { + arg = GenericTypeResolver.resolveTypeVariable((TypeVariable) arg, BindingLifecycle.class); + } + if (arg instanceof Class) { + return (Class) arg; + } + } + return null; + } + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodResolver.java b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodResolver.java new file mode 100644 index 00000000000..ee350dfa27d --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodResolver.java @@ -0,0 +1,141 @@ +/* + * Copyright 2002-2009 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 RequestMapping typeLevelMapping; + + private boolean sessionAttributesFound; + + private final Set sessionAttributeNames = new HashSet(); + + private final Set sessionAttributeTypes = new HashSet(); + + private final Set actualSessionAttributeNames = Collections.synchronizedSet(new HashSet(4)); + + + /** + * Initialize a new HandlerMethodResolver for the specified handler type. + * @param handlerType the handler class to introspect + */ + public void init(final Class handlerType) { + ReflectionUtils.doWithMethods(handlerType, new ReflectionUtils.MethodCallback() { + public void doWith(Method method) { + if (isHandlerMethod(method)) { + 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())); + } + } + + protected boolean isHandlerMethod(Method method) { + return method.isAnnotationPresent(RequestMapping.class); + } + + + 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/src/main/java/org/springframework/web/bind/annotation/support/package-info.java b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/support/package-info.java new file mode 100644 index 00000000000..c9da51e506b --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/support/package-info.java @@ -0,0 +1,8 @@ + +/** + * + * Support classes for web annotation processing. + * + */ +package org.springframework.web.bind.annotation.support; + diff --git a/org.springframework.web/src/main/java/org/springframework/web/bind/package-info.java b/org.springframework.web/src/main/java/org/springframework/web/bind/package-info.java new file mode 100644 index 00000000000..c91db7c306e --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/bind/package-info.java @@ -0,0 +1,8 @@ + +/** + * + * Provides web-specific data binding functionality. + * + */ +package org.springframework.web.bind; + diff --git a/org.springframework.web/src/main/java/org/springframework/web/bind/support/ConfigurableWebBindingInitializer.java b/org.springframework.web/src/main/java/org/springframework/web/bind/support/ConfigurableWebBindingInitializer.java new file mode 100644 index 00000000000..27a1361b0d2 --- /dev/null +++ b/org.springframework.web/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/src/main/java/org/springframework/web/bind/support/DefaultSessionAttributeStore.java b/org.springframework.web/src/main/java/org/springframework/web/bind/support/DefaultSessionAttributeStore.java new file mode 100644 index 00000000000..6f0e512a1a9 --- /dev/null +++ b/org.springframework.web/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/src/main/java/org/springframework/web/bind/support/PresentationModelUtils.java b/org.springframework.web/src/main/java/org/springframework/web/bind/support/PresentationModelUtils.java new file mode 100644 index 00000000000..02febd250c7 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/bind/support/PresentationModelUtils.java @@ -0,0 +1,65 @@ +/* + * Copyright 2004-2009 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 javax.servlet.ServletRequest; + +import org.springframework.model.ui.PresentationModelFactory; +import org.springframework.model.ui.support.DefaultPresentationModelFactory; +import org.springframework.web.context.request.WebRequest; + +/** + * Utilities for working with the model.ui PresentationModel system. + * @author Keith Donald + */ +public final class PresentationModelUtils { + + private static final String PRESENTATION_MODEL_FACTORY_ATTRIBUTE = "presentationModelFactory"; + + private PresentationModelUtils() { + } + + /** + * Get the PresentationModelFactory for the current web request. + * Will create a new one and cache it as a request attribute if one does not exist. + * @param request the web request + * @return the presentation model factory + */ + public static PresentationModelFactory getPresentationModelFactory(WebRequest request) { + PresentationModelFactory factory = (PresentationModelFactory) request.getAttribute(PRESENTATION_MODEL_FACTORY_ATTRIBUTE, WebRequest.SCOPE_REQUEST); + if (factory == null) { + factory = new DefaultPresentationModelFactory(); + request.setAttribute(PRESENTATION_MODEL_FACTORY_ATTRIBUTE, factory, WebRequest.SCOPE_REQUEST); + } + return factory; + } + + /** + * Get the PresentationModelFactory for the current servlet request. + * Will create a new one and cache it as a request attribute if one does not exist. + * @param request the servlet + * @return the presentation model factory + */ + public static PresentationModelFactory getPresentationModelFactory(ServletRequest request) { + PresentationModelFactory factory = (PresentationModelFactory) request.getAttribute(PRESENTATION_MODEL_FACTORY_ATTRIBUTE); + if (factory == null) { + factory = new DefaultPresentationModelFactory(); + request.setAttribute(PRESENTATION_MODEL_FACTORY_ATTRIBUTE, factory); + } + return factory; + } + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/bind/support/SessionAttributeStore.java b/org.springframework.web/src/main/java/org/springframework/web/bind/support/SessionAttributeStore.java new file mode 100644 index 00000000000..e428074dad5 --- /dev/null +++ b/org.springframework.web/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/src/main/java/org/springframework/web/bind/support/SessionStatus.java b/org.springframework.web/src/main/java/org/springframework/web/bind/support/SessionStatus.java new file mode 100644 index 00000000000..cf48169e9f1 --- /dev/null +++ b/org.springframework.web/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/src/main/java/org/springframework/web/bind/support/SimpleSessionStatus.java b/org.springframework.web/src/main/java/org/springframework/web/bind/support/SimpleSessionStatus.java new file mode 100644 index 00000000000..1839316574c --- /dev/null +++ b/org.springframework.web/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/src/main/java/org/springframework/web/bind/support/WebArgumentResolver.java b/org.springframework.web/src/main/java/org/springframework/web/bind/support/WebArgumentResolver.java new file mode 100644 index 00000000000..d151266be93 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/bind/support/WebArgumentResolver.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2009 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 special parameter types, resolving + * well-known argument values for them. + * + *

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

+ * public class MySpecialArgumentResolver implements WebArgumentResolver {
+ *
+ *   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/src/main/java/org/springframework/web/bind/support/WebBindingInitializer.java b/org.springframework.web/src/main/java/org/springframework/web/bind/support/WebBindingInitializer.java new file mode 100644 index 00000000000..be7e0479dee --- /dev/null +++ b/org.springframework.web/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/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java b/org.springframework.web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java new file mode 100644 index 00000000000..5f136938174 --- /dev/null +++ b/org.springframework.web/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/src/main/java/org/springframework/web/bind/support/package-info.java b/org.springframework.web/src/main/java/org/springframework/web/bind/support/package-info.java new file mode 100644 index 00000000000..f610613eda7 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/bind/support/package-info.java @@ -0,0 +1,8 @@ + +/** + * + * Support classes for web data binding. + * + */ +package org.springframework.web.bind.support; + diff --git a/org.springframework.web/src/main/java/org/springframework/web/multipart/MaxUploadSizeExceededException.java b/org.springframework.web/src/main/java/org/springframework/web/multipart/MaxUploadSizeExceededException.java new file mode 100644 index 00000000000..fe9892dcfc5 --- /dev/null +++ b/org.springframework.web/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/src/main/java/org/springframework/web/multipart/MultipartException.java b/org.springframework.web/src/main/java/org/springframework/web/multipart/MultipartException.java new file mode 100644 index 00000000000..64360f4d8d9 --- /dev/null +++ b/org.springframework.web/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/src/main/java/org/springframework/web/multipart/MultipartFile.java b/org.springframework.web/src/main/java/org/springframework/web/multipart/MultipartFile.java new file mode 100644 index 00000000000..b45a5a411e1 --- /dev/null +++ b/org.springframework.web/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/src/main/java/org/springframework/web/multipart/MultipartHttpServletRequest.java b/org.springframework.web/src/main/java/org/springframework/web/multipart/MultipartHttpServletRequest.java new file mode 100644 index 00000000000..249e508b3eb --- /dev/null +++ b/org.springframework.web/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/src/main/java/org/springframework/web/multipart/MultipartRequest.java b/org.springframework.web/src/main/java/org/springframework/web/multipart/MultipartRequest.java new file mode 100644 index 00000000000..d22ce7bde75 --- /dev/null +++ b/org.springframework.web/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/src/main/java/org/springframework/web/multipart/MultipartResolver.java b/org.springframework.web/src/main/java/org/springframework/web/multipart/MultipartResolver.java new file mode 100644 index 00000000000..0e6a5baaba1 --- /dev/null +++ b/org.springframework.web/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: + *

    + *
  • {@link org.springframework.web.multipart.commons.CommonsMultipartResolver} for Jakarta Commons FileUpload + *
+ * + *

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/src/main/java/org/springframework/web/multipart/commons/CommonsFileUploadSupport.java b/org.springframework.web/src/main/java/org/springframework/web/multipart/commons/CommonsFileUploadSupport.java new file mode 100644 index 00000000000..6ae7ca85246 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/multipart/commons/CommonsFileUploadSupport.java @@ -0,0 +1,322 @@ +/* + * 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.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.Collection; +import java.util.HashMap; +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.multipart.MultipartFile; +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 (FileItem fileItem : fileItems) { + 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 = 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 (MultipartFile file : multipartFiles) { + if (file instanceof CommonsMultipartFile) { + CommonsMultipartFile cmf = (CommonsMultipartFile) file; + cmf.getFileItem().delete(); + if (logger.isDebugEnabled()) { + logger.debug("Cleaning up multipart file [" + cmf.getName() + "] with original filename [" + + cmf.getOriginalFilename() + "], stored " + cmf.getStorageDescription()); + } + } + } + } + + + /** + * 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 mpFiles Map of field name to MultipartFile instance + * @param mpParams Map of field name to form field String value + */ + public MultipartParsingResult(Map mpFiles, Map mpParams) { + this.multipartFiles = mpFiles; + this.multipartParameters = mpParams; + } + + /** + * 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/src/main/java/org/springframework/web/multipart/commons/CommonsMultipartFile.java b/org.springframework.web/src/main/java/org/springframework/web/multipart/commons/CommonsMultipartFile.java new file mode 100644 index 00000000000..8c042953eef --- /dev/null +++ b/org.springframework.web/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/src/main/java/org/springframework/web/multipart/commons/CommonsMultipartResolver.java b/org.springframework.web/src/main/java/org/springframework/web/multipart/commons/CommonsMultipartResolver.java new file mode 100644 index 00000000000..42a72e6b1ab --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/multipart/commons/CommonsMultipartResolver.java @@ -0,0 +1,195 @@ +/* + * 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.FileItem; +import org.apache.commons.fileupload.servlet.ServletFileUpload; + +import org.springframework.util.Assert; +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.2 or above. + * + *

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 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 + */ + @Override + 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) { + return (request != null && ServletFileUpload.isMultipartContent(request)); + } + + public MultipartHttpServletRequest resolveMultipart(final HttpServletRequest request) throws MultipartException { + Assert.notNull(request, "Request must not be null"); + if (this.resolveLazily) { + return new DefaultMultipartHttpServletRequest(request) { + @Override + 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. + */ + @SuppressWarnings("unchecked") + 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/src/main/java/org/springframework/web/multipart/commons/package-info.java b/org.springframework.web/src/main/java/org/springframework/web/multipart/commons/package-info.java new file mode 100644 index 00000000000..931eaf305a4 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/multipart/commons/package-info.java @@ -0,0 +1,9 @@ + +/** + * + * MultipartResolver implementation for + * Jakarta Commons FileUpload. + * + */ +package org.springframework.web.multipart.commons; + diff --git a/org.springframework.web/src/main/java/org/springframework/web/multipart/package-info.java b/org.springframework.web/src/main/java/org/springframework/web/multipart/package-info.java new file mode 100644 index 00000000000..2de262ce41d --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/multipart/package-info.java @@ -0,0 +1,11 @@ + +/** + * + * 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. + * + */ +package org.springframework.web.multipart; + diff --git a/org.springframework.web/src/main/java/org/springframework/web/multipart/support/AbstractMultipartHttpServletRequest.java b/org.springframework.web/src/main/java/org/springframework/web/multipart/support/AbstractMultipartHttpServletRequest.java new file mode 100644 index 00000000000..cac07d02ba7 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/multipart/support/AbstractMultipartHttpServletRequest.java @@ -0,0 +1,92 @@ +/* + * 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.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 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/src/main/java/org/springframework/web/multipart/support/ByteArrayMultipartFileEditor.java b/org.springframework.web/src/main/java/org/springframework/web/multipart/support/ByteArrayMultipartFileEditor.java new file mode 100644 index 00000000000..ee8e34392a5 --- /dev/null +++ b/org.springframework.web/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 { + + @Override + public void setValue(Object value) { + if (value instanceof MultipartFile) { + MultipartFile multipartFile = (MultipartFile) value; + try { + super.setValue(multipartFile.getBytes()); + } + catch (IOException ex) { + throw new IllegalArgumentException("Cannot read contents of multipart file", ex); + } + } + else if (value instanceof byte[]) { + super.setValue(value); + } + else { + super.setValue(value != null ? value.toString().getBytes() : null); + } + } + + @Override + public String getAsText() { + byte[] value = (byte[]) getValue(); + return (value != null ? new String(value) : ""); + } + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/multipart/support/DefaultMultipartHttpServletRequest.java b/org.springframework.web/src/main/java/org/springframework/web/multipart/support/DefaultMultipartHttpServletRequest.java new file mode 100644 index 00000000000..656abf30d15 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/multipart/support/DefaultMultipartHttpServletRequest.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.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; + +import org.springframework.web.multipart.MultipartFile; + +/** + * 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 mpFiles a map of the multipart files + * @param mpParams a map of the parameters to expose, + * with Strings as keys and String arrays as values + */ + public DefaultMultipartHttpServletRequest( + HttpServletRequest request, Map mpFiles, Map mpParams) { + + super(request); + setMultipartFiles(mpFiles); + setMultipartParameters(mpParams); + } + + /** + * Wrap the given HttpServletRequest in a MultipartHttpServletRequest. + * @param request the servlet request to wrap + */ + public DefaultMultipartHttpServletRequest(HttpServletRequest request) { + super(request); + } + + + @Override + public Enumeration getParameterNames() { + Set paramNames = new HashSet(); + Enumeration paramEnum = super.getParameterNames(); + while (paramEnum.hasMoreElements()) { + paramNames.add((String) paramEnum.nextElement()); + } + paramNames.addAll(getMultipartParameters().keySet()); + return Collections.enumeration(paramNames); + } + + @Override + public String getParameter(String name) { + String[] values = getMultipartParameters().get(name); + if (values != null) { + return (values.length > 0 ? values[0] : null); + } + return super.getParameter(name); + } + + @Override + public String[] getParameterValues(String name) { + String[] values = getMultipartParameters().get(name); + if (values != null) { + return values; + } + return super.getParameterValues(name); + } + + @Override + @SuppressWarnings("unchecked") + 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/src/main/java/org/springframework/web/multipart/support/MultipartFilter.java b/org.springframework.web/src/main/java/org/springframework/web/multipart/support/MultipartFilter.java new file mode 100644 index 00000000000..b5c770bf85d --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/multipart/support/MultipartFilter.java @@ -0,0 +1,151 @@ +/* + * 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. + */ + @Override + 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/src/main/java/org/springframework/web/multipart/support/StringMultipartFileEditor.java b/org.springframework.web/src/main/java/org/springframework/web/multipart/support/StringMultipartFileEditor.java new file mode 100644 index 00000000000..9c551c315f2 --- /dev/null +++ b/org.springframework.web/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; + } + + + @Override + public void setAsText(String text) { + setValue(text); + } + + @Override + 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) { + throw new IllegalArgumentException("Cannot read contents of multipart file", ex); + } + } + else { + super.setValue(value); + } + } + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/multipart/support/package-info.java b/org.springframework.web/src/main/java/org/springframework/web/multipart/support/package-info.java new file mode 100644 index 00000000000..5b12543c781 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/multipart/support/package-info.java @@ -0,0 +1,10 @@ + +/** + * + * 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. + * + */ +package org.springframework.web.multipart.support; + diff --git a/org.springframework.web/src/test/java/org/springframework/mock/web/PassThroughFilterChain.java b/org.springframework.web/src/test/java/org/springframework/mock/web/PassThroughFilterChain.java new file mode 100644 index 00000000000..3b22b1f966a --- /dev/null +++ b/org.springframework.web/src/test/java/org/springframework/mock/web/PassThroughFilterChain.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2009 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.mock.web; + +import java.io.IOException; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.Servlet; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import org.springframework.util.Assert; + +/** + * Implementation of the {@link javax.servlet.FilterConfig} interface which + * simply passes the call through to a given Filter/FilterChain combo + * (indicating the next Filter in the chain along with the FilterChain that it is + * supposed to work on) or to a given Servlet (indicating the end of the chain). + * + * @author Juergen Hoeller + * @since 2.0.3 + * @see javax.servlet.Filter + * @see javax.servlet.Servlet + * @see MockFilterChain + */ +public class PassThroughFilterChain implements FilterChain { + + private Filter filter; + + private FilterChain nextFilterChain; + + private Servlet servlet; + + + /** + * Create a new PassThroughFilterChain that delegates to the given Filter, + * calling it with the given FilterChain. + * @param filter the Filter to delegate to + * @param nextFilterChain the FilterChain to use for that next Filter + */ + public PassThroughFilterChain(Filter filter, FilterChain nextFilterChain) { + Assert.notNull(filter, "Filter must not be null"); + Assert.notNull(nextFilterChain, "'FilterChain must not be null"); + this.filter = filter; + this.nextFilterChain = nextFilterChain; + } + + /** + * Create a new PassThroughFilterChain that delegates to the given Servlet. + * @param servlet the Servlet to delegate to + */ + public PassThroughFilterChain(Servlet servlet) { + Assert.notNull(servlet, "Servlet must not be null"); + this.servlet = servlet; + } + + + /** + * Pass the call on to the Filter/Servlet. + */ + public void doFilter(ServletRequest request, ServletResponse response) throws ServletException, IOException { + if (this.filter != null) { + this.filter.doFilter(request, response, this.nextFilterChain); + } + else { + this.servlet.service(request, response); + } + } + +} diff --git a/org.springframework.web/src/test/java/org/springframework/web/bind/EscapedErrorsTests.java b/org.springframework.web/src/test/java/org/springframework/web/bind/EscapedErrorsTests.java new file mode 100644 index 00000000000..a279ceab4b2 --- /dev/null +++ b/org.springframework.web/src/test/java/org/springframework/web/bind/EscapedErrorsTests.java @@ -0,0 +1,90 @@ +/* + * 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 junit.framework.TestCase; + +import org.springframework.beans.TestBean; +import org.springframework.validation.BindException; +import org.springframework.validation.Errors; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; + +/** + * @author Juergen Hoeller + * @since 02.05.2003 + */ +public class EscapedErrorsTests extends TestCase { + + public void testEscapedErrors() { + TestBean tb = new TestBean(); + tb.setName("empty &"); + + Errors errors = new EscapedErrors(new BindException(tb, "tb")); + errors.rejectValue("name", "NAME_EMPTY &", null, "message: &"); + errors.rejectValue("age", "AGE_NOT_SET ", null, "message: "); + errors.rejectValue("age", "AGE_NOT_32 ", null, "message: "); + errors.reject("GENERAL_ERROR \" '", null, "message: \" '"); + + assertTrue("Correct errors flag", errors.hasErrors()); + assertTrue("Correct number of errors", errors.getErrorCount() == 4); + assertTrue("Correct object name", "tb".equals(errors.getObjectName())); + + assertTrue("Correct global errors flag", errors.hasGlobalErrors()); + assertTrue("Correct number of global errors", errors.getGlobalErrorCount() == 1); + ObjectError globalError = errors.getGlobalError(); + assertTrue("Global error message escaped", "message: " '".equals(globalError.getDefaultMessage())); + assertTrue("Global error code not escaped", "GENERAL_ERROR \" '".equals(globalError.getCode())); + ObjectError globalErrorInList = (ObjectError) errors.getGlobalErrors().get(0); + assertTrue("Same global error in list", globalError.getDefaultMessage().equals(globalErrorInList.getDefaultMessage())); + ObjectError globalErrorInAllList = (ObjectError) errors.getAllErrors().get(3); + assertTrue("Same global error in list", globalError.getDefaultMessage().equals(globalErrorInAllList.getDefaultMessage())); + + assertTrue("Correct field errors flag", errors.hasFieldErrors()); + assertTrue("Correct number of field errors", errors.getFieldErrorCount() == 3); + assertTrue("Correct number of field errors in list", errors.getFieldErrors().size() == 3); + FieldError fieldError = errors.getFieldError(); + assertTrue("Field error code not escaped", "NAME_EMPTY &".equals(fieldError.getCode())); + assertTrue("Field value escaped", "empty &".equals(errors.getFieldValue("name"))); + FieldError fieldErrorInList = (FieldError) errors.getFieldErrors().get(0); + assertTrue("Same field error in list", fieldError.getDefaultMessage().equals(fieldErrorInList.getDefaultMessage())); + + assertTrue("Correct name errors flag", errors.hasFieldErrors("name")); + assertTrue("Correct number of name errors", errors.getFieldErrorCount("name") == 1); + assertTrue("Correct number of name errors in list", errors.getFieldErrors("name").size() == 1); + FieldError nameError = errors.getFieldError("name"); + assertTrue("Name error message escaped", "message: &".equals(nameError.getDefaultMessage())); + assertTrue("Name error code not escaped", "NAME_EMPTY &".equals(nameError.getCode())); + assertTrue("Name value escaped", "empty &".equals(errors.getFieldValue("name"))); + FieldError nameErrorInList = (FieldError) errors.getFieldErrors("name").get(0); + assertTrue("Same name error in list", nameError.getDefaultMessage().equals(nameErrorInList.getDefaultMessage())); + + assertTrue("Correct age errors flag", errors.hasFieldErrors("age")); + assertTrue("Correct number of age errors", errors.getFieldErrorCount("age") == 2); + assertTrue("Correct number of age errors in list", errors.getFieldErrors("age").size() == 2); + FieldError ageError = errors.getFieldError("age"); + assertTrue("Age error message escaped", "message: <tag>".equals(ageError.getDefaultMessage())); + assertTrue("Age error code not escaped", "AGE_NOT_SET ".equals(ageError.getCode())); + assertTrue("Age value not escaped", (new Integer(0)).equals(errors.getFieldValue("age"))); + FieldError ageErrorInList = (FieldError) errors.getFieldErrors("age").get(0); + assertTrue("Same name error in list", ageError.getDefaultMessage().equals(ageErrorInList.getDefaultMessage())); + FieldError ageError2 = (FieldError) errors.getFieldErrors("age").get(1); + assertTrue("Age error 2 message escaped", "message: <tag>".equals(ageError2.getDefaultMessage())); + assertTrue("Age error 2 code not escaped", "AGE_NOT_32 ".equals(ageError2.getCode())); + } + +} diff --git a/org.springframework.web/src/test/java/org/springframework/web/bind/ServletRequestDataBinderTests.java b/org.springframework.web/src/test/java/org/springframework/web/bind/ServletRequestDataBinderTests.java new file mode 100644 index 00000000000..aa7bd4f9de0 --- /dev/null +++ b/org.springframework.web/src/test/java/org/springframework/web/bind/ServletRequestDataBinderTests.java @@ -0,0 +1,256 @@ +/* + * 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 static org.junit.Assert.*; + +import java.beans.PropertyEditorSupport; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; +import org.springframework.beans.ITestBean; +import org.springframework.beans.PropertyValue; +import org.springframework.beans.PropertyValues; +import org.springframework.beans.TestBean; +import org.springframework.mock.web.MockHttpServletRequest; + +/** + * @author Rod Johnson + * @author Juergen Hoeller + * @author Chris Beams + * @author Scott Andrews + */ +public class ServletRequestDataBinderTests { + + @Test + public void testBindingWithNestedObjectCreation() throws Exception { + TestBean tb = new TestBean(); + + ServletRequestDataBinder binder = new ServletRequestDataBinder(tb, "person"); + binder.registerCustomEditor(ITestBean.class, new PropertyEditorSupport() { + public void setAsText(String text) throws IllegalArgumentException { + setValue(new TestBean()); + } + }); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("spouse", "someValue"); + request.addParameter("spouse.name", "test"); + binder.bind(request); + + assertNotNull(tb.getSpouse()); + assertEquals("test", tb.getSpouse().getName()); + } + + @Test + public void testFieldPrefixCausesFieldReset() throws Exception { + TestBean target = new TestBean(); + ServletRequestDataBinder binder = new ServletRequestDataBinder(target); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("_postProcessed", "visible"); + request.addParameter("postProcessed", "on"); + binder.bind(request); + assertTrue(target.isPostProcessed()); + + request.removeParameter("postProcessed"); + binder.bind(request); + assertFalse(target.isPostProcessed()); + } + + @Test + public void testFieldPrefixCausesFieldResetWithIgnoreUnknownFields() throws Exception { + TestBean target = new TestBean(); + ServletRequestDataBinder binder = new ServletRequestDataBinder(target); + binder.setIgnoreUnknownFields(false); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("_postProcessed", "visible"); + request.addParameter("postProcessed", "on"); + binder.bind(request); + assertTrue(target.isPostProcessed()); + + request.removeParameter("postProcessed"); + binder.bind(request); + assertFalse(target.isPostProcessed()); + } + + @Test + public void testFieldDefault() throws Exception { + TestBean target = new TestBean(); + ServletRequestDataBinder binder = new ServletRequestDataBinder(target); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("!postProcessed", "off"); + request.addParameter("postProcessed", "on"); + binder.bind(request); + assertTrue(target.isPostProcessed()); + + request.removeParameter("postProcessed"); + binder.bind(request); + assertFalse(target.isPostProcessed()); + } + + @Test + public void testFieldDefaultPreemptsFieldMarker() throws Exception { + TestBean target = new TestBean(); + ServletRequestDataBinder binder = new ServletRequestDataBinder(target); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("!postProcessed", "on"); + request.addParameter("_postProcessed", "visible"); + request.addParameter("postProcessed", "on"); + binder.bind(request); + assertTrue(target.isPostProcessed()); + + request.removeParameter("postProcessed"); + binder.bind(request); + assertTrue(target.isPostProcessed()); + + request.removeParameter("!postProcessed"); + binder.bind(request); + assertFalse(target.isPostProcessed()); + } + + @Test + public void testFieldDefaultNonBoolean() throws Exception { + TestBean target = new TestBean(); + ServletRequestDataBinder binder = new ServletRequestDataBinder(target); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("!name", "anonymous"); + request.addParameter("name", "Scott"); + binder.bind(request); + assertEquals("Scott", target.getName()); + + request.removeParameter("name"); + binder.bind(request); + assertEquals("anonymous", target.getName()); + } + + @Test + public void testWithCommaSeparatedStringArray() throws Exception { + TestBean target = new TestBean(); + ServletRequestDataBinder binder = new ServletRequestDataBinder(target); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("stringArray", "bar"); + request.addParameter("stringArray", "abc"); + request.addParameter("stringArray", "123,def"); + binder.bind(request); + assertEquals("Expected all three items to be bound", 3, target.getStringArray().length); + + request.removeParameter("stringArray"); + request.addParameter("stringArray", "123,def"); + binder.bind(request); + assertEquals("Expected only 1 item to be bound", 1, target.getStringArray().length); + } + + @Test + public void testBindingWithNestedObjectCreationAndWrongOrder() throws Exception { + TestBean tb = new TestBean(); + + ServletRequestDataBinder binder = new ServletRequestDataBinder(tb, "person"); + binder.registerCustomEditor(ITestBean.class, new PropertyEditorSupport() { + public void setAsText(String text) throws IllegalArgumentException { + setValue(new TestBean()); + } + }); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("spouse.name", "test"); + request.addParameter("spouse", "someValue"); + binder.bind(request); + + assertNotNull(tb.getSpouse()); + assertEquals("test", tb.getSpouse().getName()); + } + + @Test + public void testNoPrefix() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("forname", "Tony"); + request.addParameter("surname", "Blair"); + request.addParameter("age", "" + 50); + + ServletRequestParameterPropertyValues pvs = new ServletRequestParameterPropertyValues(request); + doTestTony(pvs); + } + + @Test + public void testPrefix() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("test_forname", "Tony"); + request.addParameter("test_surname", "Blair"); + request.addParameter("test_age", "" + 50); + + ServletRequestParameterPropertyValues pvs = new ServletRequestParameterPropertyValues(request); + assertTrue("Didn't fidn normal when given prefix", !pvs.contains("forname")); + assertTrue("Did treat prefix as normal when not given prefix", pvs.contains("test_forname")); + + pvs = new ServletRequestParameterPropertyValues(request, "test"); + doTestTony(pvs); + } + + @Test + public void testNoParameters() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + ServletRequestParameterPropertyValues pvs = new ServletRequestParameterPropertyValues(request); + assertTrue("Found no parameters", pvs.getPropertyValues().length == 0); + } + + @Test + public void testMultipleValuesForParameter() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + String[] original = new String[] {"Tony", "Rod"}; + request.addParameter("forname", original); + + ServletRequestParameterPropertyValues pvs = new ServletRequestParameterPropertyValues(request); + assertTrue("Found 1 parameter", pvs.getPropertyValues().length == 1); + assertTrue("Found array value", pvs.getPropertyValue("forname").getValue() instanceof String[]); + String[] values = (String[]) pvs.getPropertyValue("forname").getValue(); + assertEquals("Correct values", Arrays.asList(values), Arrays.asList(original)); + } + + /** + * Must contain: forname=Tony surname=Blair age=50 + */ + protected void doTestTony(PropertyValues pvs) throws Exception { + assertTrue("Contains 3", pvs.getPropertyValues().length == 3); + assertTrue("Contains forname", pvs.contains("forname")); + assertTrue("Contains surname", pvs.contains("surname")); + assertTrue("Contains age", pvs.contains("age")); + assertTrue("Doesn't contain tory", !pvs.contains("tory")); + + PropertyValue[] ps = pvs.getPropertyValues(); + Map m = new HashMap(); + m.put("forname", "Tony"); + m.put("surname", "Blair"); + m.put("age", "50"); + for (int i = 0; i < ps.length; i++) { + Object val = m.get(ps[i].getName()); + assertTrue("Can't have unexpected value", val != null); + assertTrue("Val i string", val instanceof String); + assertTrue("val matches expected", val.equals(ps[i].getValue())); + m.remove(ps[i].getName()); + } + assertTrue("Map size is 0", m.size() == 0); + } + +} diff --git a/org.springframework.web/src/test/java/org/springframework/web/bind/ServletRequestUtilsTests.java b/org.springframework.web/src/test/java/org/springframework/web/bind/ServletRequestUtilsTests.java new file mode 100644 index 00000000000..095be263635 --- /dev/null +++ b/org.springframework.web/src/test/java/org/springframework/web/bind/ServletRequestUtilsTests.java @@ -0,0 +1,465 @@ +/* + * 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 static org.junit.Assert.*; + +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.util.StopWatch; + +/** + * @author Juergen Hoeller + * @author Chris Beams + * @since 06.08.2003 + */ +public class ServletRequestUtilsTests { + + @Test + public void testIntParameter() throws ServletRequestBindingException { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("param1", "5"); + request.addParameter("param2", "e"); + request.addParameter("paramEmpty", ""); + + assertEquals(ServletRequestUtils.getIntParameter(request, "param1"), new Integer(5)); + assertEquals(ServletRequestUtils.getIntParameter(request, "param1", 6), 5); + assertEquals(ServletRequestUtils.getRequiredIntParameter(request, "param1"), 5); + + assertEquals(ServletRequestUtils.getIntParameter(request, "param2", 6), 6); + try { + ServletRequestUtils.getRequiredIntParameter(request, "param2"); + fail("Should have thrown ServletRequestBindingException"); + } + catch (ServletRequestBindingException ex) { + // expected + } + + assertEquals(ServletRequestUtils.getIntParameter(request, "param3"), null); + assertEquals(ServletRequestUtils.getIntParameter(request, "param3", 6), 6); + try { + ServletRequestUtils.getRequiredIntParameter(request, "param3"); + fail("Should have thrown ServletRequestBindingException"); + } + catch (ServletRequestBindingException ex) { + // expected + } + + try { + ServletRequestUtils.getRequiredIntParameter(request, "paramEmpty"); + fail("Should have thrown ServletRequestBindingException"); + } + catch (ServletRequestBindingException ex) { + // expected + } + } + + @Test + public void testIntParameters() throws ServletRequestBindingException { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("param", new String[] {"1", "2", "3"}); + + request.addParameter("param2", "1"); + request.addParameter("param2", "2"); + request.addParameter("param2", "bogus"); + + int[] array = new int[] {1, 2, 3}; + int[] values = ServletRequestUtils.getRequiredIntParameters(request, "param"); + assertEquals(3, values.length); + for (int i = 0; i < array.length; i++) { + assertEquals(array[i], values[i]); + } + + try { + ServletRequestUtils.getRequiredIntParameters(request, "param2"); + fail("Should have thrown ServletRequestBindingException"); + } + catch (ServletRequestBindingException ex) { + // expected + } + } + + @Test + public void testLongParameter() throws ServletRequestBindingException { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("param1", "5"); + request.addParameter("param2", "e"); + request.addParameter("paramEmpty", ""); + + assertEquals(ServletRequestUtils.getLongParameter(request, "param1"), new Long(5L)); + assertEquals(ServletRequestUtils.getLongParameter(request, "param1", 6L), 5L); + assertEquals(ServletRequestUtils.getRequiredIntParameter(request, "param1"), 5L); + + assertEquals(ServletRequestUtils.getLongParameter(request, "param2", 6L), 6L); + try { + ServletRequestUtils.getRequiredLongParameter(request, "param2"); + fail("Should have thrown ServletRequestBindingException"); + } + catch (ServletRequestBindingException ex) { + // expected + } + + assertEquals(ServletRequestUtils.getLongParameter(request, "param3"), null); + assertEquals(ServletRequestUtils.getLongParameter(request, "param3", 6L), 6L); + try { + ServletRequestUtils.getRequiredLongParameter(request, "param3"); + fail("Should have thrown ServletRequestBindingException"); + } + catch (ServletRequestBindingException ex) { + // expected + } + + try { + ServletRequestUtils.getRequiredLongParameter(request, "paramEmpty"); + fail("Should have thrown ServletRequestBindingException"); + } + catch (ServletRequestBindingException ex) { + // expected + } + } + + @Test + public void testLongParameters() throws ServletRequestBindingException { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setParameter("param", new String[] {"1", "2", "3"}); + + request.setParameter("param2", "0"); + request.setParameter("param2", "1"); + request.addParameter("param2", "2"); + request.addParameter("param2", "bogus"); + + long[] array = new long[] {1L, 2L, 3L}; + long[] values = ServletRequestUtils.getRequiredLongParameters(request, "param"); + assertEquals(3, values.length); + for (int i = 0; i < array.length; i++) { + assertEquals(array[i], values[i]); + } + + try { + ServletRequestUtils.getRequiredLongParameters(request, "param2"); + fail("Should have thrown ServletRequestBindingException"); + } + catch (ServletRequestBindingException ex) { + // expected + } + + request.setParameter("param2", new String[] {"1", "2"}); + values = ServletRequestUtils.getRequiredLongParameters(request, "param2"); + assertEquals(2, values.length); + assertEquals(1, values[0]); + assertEquals(2, values[1]); + + request.removeParameter("param2"); + try { + ServletRequestUtils.getRequiredLongParameters(request, "param2"); + fail("Should have thrown ServletRequestBindingException"); + } + catch (ServletRequestBindingException ex) { + // expected + } + } + + @Test + public void testFloatParameter() throws ServletRequestBindingException { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("param1", "5.5"); + request.addParameter("param2", "e"); + request.addParameter("paramEmpty", ""); + + assertTrue(ServletRequestUtils.getFloatParameter(request, "param1").equals(new Float(5.5f))); + assertTrue(ServletRequestUtils.getFloatParameter(request, "param1", 6.5f) == 5.5f); + assertTrue(ServletRequestUtils.getRequiredFloatParameter(request, "param1") == 5.5f); + + assertTrue(ServletRequestUtils.getFloatParameter(request, "param2", 6.5f) == 6.5f); + try { + ServletRequestUtils.getRequiredFloatParameter(request, "param2"); + fail("Should have thrown ServletRequestBindingException"); + } + catch (ServletRequestBindingException ex) { + // expected + } + + assertTrue(ServletRequestUtils.getFloatParameter(request, "param3") == null); + assertTrue(ServletRequestUtils.getFloatParameter(request, "param3", 6.5f) == 6.5f); + try { + ServletRequestUtils.getRequiredFloatParameter(request, "param3"); + fail("Should have thrown ServletRequestBindingException"); + } + catch (ServletRequestBindingException ex) { + // expected + } + + try { + ServletRequestUtils.getRequiredFloatParameter(request, "paramEmpty"); + fail("Should have thrown ServletRequestBindingException"); + } + catch (ServletRequestBindingException ex) { + // expected + } + } + + @Test + public void testFloatParameters() throws ServletRequestBindingException { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("param", new String[] {"1.5", "2.5", "3"}); + + request.addParameter("param2", "1.5"); + request.addParameter("param2", "2"); + request.addParameter("param2", "bogus"); + + float[] array = new float[] {1.5F, 2.5F, 3}; + float[] values = ServletRequestUtils.getRequiredFloatParameters(request, "param"); + assertEquals(3, values.length); + for (int i = 0; i < array.length; i++) { + assertEquals(array[i], values[i], 0); + } + + try { + ServletRequestUtils.getRequiredFloatParameters(request, "param2"); + fail("Should have thrown ServletRequestBindingException"); + } + catch (ServletRequestBindingException ex) { + // expected + } + } + + @Test + public void testDoubleParameter() throws ServletRequestBindingException { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("param1", "5.5"); + request.addParameter("param2", "e"); + request.addParameter("paramEmpty", ""); + + assertTrue(ServletRequestUtils.getDoubleParameter(request, "param1").equals(new Double(5.5))); + assertTrue(ServletRequestUtils.getDoubleParameter(request, "param1", 6.5) == 5.5); + assertTrue(ServletRequestUtils.getRequiredDoubleParameter(request, "param1") == 5.5); + + assertTrue(ServletRequestUtils.getDoubleParameter(request, "param2", 6.5) == 6.5); + try { + ServletRequestUtils.getRequiredDoubleParameter(request, "param2"); + fail("Should have thrown ServletRequestBindingException"); + } + catch (ServletRequestBindingException ex) { + // expected + } + + assertTrue(ServletRequestUtils.getDoubleParameter(request, "param3") == null); + assertTrue(ServletRequestUtils.getDoubleParameter(request, "param3", 6.5) == 6.5); + try { + ServletRequestUtils.getRequiredDoubleParameter(request, "param3"); + fail("Should have thrown ServletRequestBindingException"); + } + catch (ServletRequestBindingException ex) { + // expected + } + + try { + ServletRequestUtils.getRequiredDoubleParameter(request, "paramEmpty"); + fail("Should have thrown ServletRequestBindingException"); + } + catch (ServletRequestBindingException ex) { + // expected + } + } + + @Test + public void testDoubleParameters() throws ServletRequestBindingException { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("param", new String[] {"1.5", "2.5", "3"}); + + request.addParameter("param2", "1.5"); + request.addParameter("param2", "2"); + request.addParameter("param2", "bogus"); + + double[] array = new double[] {1.5, 2.5, 3}; + double[] values = ServletRequestUtils.getRequiredDoubleParameters(request, "param"); + assertEquals(3, values.length); + for (int i = 0; i < array.length; i++) { + assertEquals(array[i], values[i], 0); + } + + try { + ServletRequestUtils.getRequiredDoubleParameters(request, "param2"); + fail("Should have thrown ServletRequestBindingException"); + } + catch (ServletRequestBindingException ex) { + // expected + } + } + + @Test + public void testBooleanParameter() throws ServletRequestBindingException { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("param1", "true"); + request.addParameter("param2", "e"); + request.addParameter("param4", "yes"); + request.addParameter("param5", "1"); + request.addParameter("paramEmpty", ""); + + assertTrue(ServletRequestUtils.getBooleanParameter(request, "param1").equals(Boolean.TRUE)); + assertTrue(ServletRequestUtils.getBooleanParameter(request, "param1", false)); + assertTrue(ServletRequestUtils.getRequiredBooleanParameter(request, "param1")); + + assertFalse(ServletRequestUtils.getBooleanParameter(request, "param2", true)); + assertFalse(ServletRequestUtils.getRequiredBooleanParameter(request, "param2")); + + assertTrue(ServletRequestUtils.getBooleanParameter(request, "param3") == null); + assertTrue(ServletRequestUtils.getBooleanParameter(request, "param3", true)); + try { + ServletRequestUtils.getRequiredBooleanParameter(request, "param3"); + fail("Should have thrown ServletRequestBindingException"); + } + catch (ServletRequestBindingException ex) { + // expected + } + + assertTrue(ServletRequestUtils.getBooleanParameter(request, "param4", false)); + assertTrue(ServletRequestUtils.getRequiredBooleanParameter(request, "param4")); + + assertTrue(ServletRequestUtils.getBooleanParameter(request, "param5", false)); + assertTrue(ServletRequestUtils.getRequiredBooleanParameter(request, "param5")); + assertFalse(ServletRequestUtils.getRequiredBooleanParameter(request, "paramEmpty")); + } + + @Test + public void testBooleanParameters() throws ServletRequestBindingException { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("param", new String[] {"true", "yes", "off", "1", "bogus"}); + + request.addParameter("param2", "false"); + request.addParameter("param2", "true"); + request.addParameter("param2", ""); + + boolean[] array = new boolean[] {true, true, false, true, false}; + boolean[] values = ServletRequestUtils.getRequiredBooleanParameters(request, "param"); + assertEquals(array.length, values.length); + for (int i = 0; i < array.length; i++) { + assertEquals(array[i], values[i]); + } + + array = new boolean[] {false, true, false}; + values = ServletRequestUtils.getRequiredBooleanParameters(request, "param2"); + assertEquals(array.length, values.length); + for (int i = 0; i < array.length; i++) { + assertEquals(array[i], values[i]); + } + } + + @Test + public void testStringParameter() throws ServletRequestBindingException { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("param1", "str"); + request.addParameter("paramEmpty", ""); + + assertEquals("str", ServletRequestUtils.getStringParameter(request, "param1")); + assertEquals("str", ServletRequestUtils.getStringParameter(request, "param1", "string")); + assertEquals("str", ServletRequestUtils.getRequiredStringParameter(request, "param1")); + + assertEquals(null, ServletRequestUtils.getStringParameter(request, "param3")); + assertEquals("string", ServletRequestUtils.getStringParameter(request, "param3", "string")); + assertNull(ServletRequestUtils.getStringParameter(request, "param3", null)); + try { + ServletRequestUtils.getRequiredStringParameter(request, "param3"); + fail("Should have thrown ServletRequestBindingException"); + } + catch (ServletRequestBindingException ex) { + // expected + } + + assertEquals("", ServletRequestUtils.getStringParameter(request, "paramEmpty")); + assertEquals("", ServletRequestUtils.getRequiredStringParameter(request, "paramEmpty")); + } + + @Test + public void testGetIntParameterWithDefaultValueHandlingIsFastEnough() { + MockHttpServletRequest request = new MockHttpServletRequest(); + StopWatch sw = new StopWatch(); + sw.start(); + for (int i = 0; i < 1000000; i++) { + ServletRequestUtils.getIntParameter(request, "nonExistingParam", 0); + } + sw.stop(); + System.out.println(sw.getTotalTimeMillis()); + assertTrue("getStringParameter took too long: " + sw.getTotalTimeMillis(), sw.getTotalTimeMillis() < 250); + } + + @Test + public void testGetLongParameterWithDefaultValueHandlingIsFastEnough() { + MockHttpServletRequest request = new MockHttpServletRequest(); + StopWatch sw = new StopWatch(); + sw.start(); + for (int i = 0; i < 1000000; i++) { + ServletRequestUtils.getLongParameter(request, "nonExistingParam", 0); + } + sw.stop(); + System.out.println(sw.getTotalTimeMillis()); + assertTrue("getStringParameter took too long: " + sw.getTotalTimeMillis(), sw.getTotalTimeMillis() < 250); + } + + @Test + public void testGetFloatParameterWithDefaultValueHandlingIsFastEnough() { + MockHttpServletRequest request = new MockHttpServletRequest(); + StopWatch sw = new StopWatch(); + sw.start(); + for (int i = 0; i < 1000000; i++) { + ServletRequestUtils.getFloatParameter(request, "nonExistingParam", 0f); + } + sw.stop(); + System.out.println(sw.getTotalTimeMillis()); + assertTrue("getStringParameter took too long: " + sw.getTotalTimeMillis(), sw.getTotalTimeMillis() < 250); + } + + @Test + public void testGetDoubleParameterWithDefaultValueHandlingIsFastEnough() { + MockHttpServletRequest request = new MockHttpServletRequest(); + StopWatch sw = new StopWatch(); + sw.start(); + for (int i = 0; i < 1000000; i++) { + ServletRequestUtils.getDoubleParameter(request, "nonExistingParam", 0d); + } + sw.stop(); + System.out.println(sw.getTotalTimeMillis()); + assertTrue("getStringParameter took too long: " + sw.getTotalTimeMillis(), sw.getTotalTimeMillis() < 250); + } + + @Test + public void testGetBooleanParameterWithDefaultValueHandlingIsFastEnough() { + MockHttpServletRequest request = new MockHttpServletRequest(); + StopWatch sw = new StopWatch(); + sw.start(); + for (int i = 0; i < 1000000; i++) { + ServletRequestUtils.getBooleanParameter(request, "nonExistingParam", false); + } + sw.stop(); + System.out.println(sw.getTotalTimeMillis()); + assertTrue("getStringParameter took too long: " + sw.getTotalTimeMillis(), sw.getTotalTimeMillis() < 250); + } + + @Test + public void testGetStringParameterWithDefaultValueHandlingIsFastEnough() { + MockHttpServletRequest request = new MockHttpServletRequest(); + StopWatch sw = new StopWatch(); + sw.start(); + for (int i = 0; i < 1000000; i++) { + ServletRequestUtils.getStringParameter(request, "nonExistingParam", "defaultValue"); + } + sw.stop(); + System.out.println(sw.getTotalTimeMillis()); + assertTrue("getStringParameter took too long: " + sw.getTotalTimeMillis(), sw.getTotalTimeMillis() < 250); + } + +} diff --git a/org.springframework.web/src/test/java/org/springframework/web/multipart/commons/CommonsMultipartResolverTests.java b/org.springframework.web/src/test/java/org/springframework/web/multipart/commons/CommonsMultipartResolverTests.java new file mode 100644 index 00000000000..6a4a65ac810 --- /dev/null +++ b/org.springframework.web/src/test/java/org/springframework/web/multipart/commons/CommonsMultipartResolverTests.java @@ -0,0 +1,485 @@ +/* + * 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.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import junit.framework.TestCase; + +import org.apache.commons.fileupload.FileItem; +import org.apache.commons.fileupload.FileItemFactory; +import org.apache.commons.fileupload.FileUpload; +import org.apache.commons.fileupload.servlet.ServletFileUpload; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.mock.web.MockFilterConfig; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockServletContext; +import org.springframework.mock.web.PassThroughFilterChain; +import org.springframework.web.bind.ServletRequestDataBinder; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.StaticWebApplicationContext; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartHttpServletRequest; +import org.springframework.web.multipart.support.ByteArrayMultipartFileEditor; +import org.springframework.web.multipart.support.MultipartFilter; +import org.springframework.web.multipart.support.StringMultipartFileEditor; +import org.springframework.web.util.WebUtils; + +/** + * @author Juergen Hoeller + * @since 08.10.2003 + */ +public class CommonsMultipartResolverTests extends TestCase { + + public void testWithApplicationContext() throws Exception { + doTestWithApplicationContext(false); + } + + public void testWithApplicationContextAndLazyResolution() throws Exception { + doTestWithApplicationContext(true); + } + + private void doTestWithApplicationContext(boolean lazy) throws Exception { + StaticWebApplicationContext wac = new StaticWebApplicationContext(); + wac.setServletContext(new MockServletContext()); + wac.getServletContext().setAttribute(WebUtils.TEMP_DIR_CONTEXT_ATTRIBUTE, new File("mytemp")); + wac.refresh(); + MockCommonsMultipartResolver resolver = new MockCommonsMultipartResolver(); + resolver.setMaxUploadSize(1000); + resolver.setMaxInMemorySize(100); + resolver.setDefaultEncoding("enc"); + if (lazy) { + resolver.setResolveLazily(false); + } + resolver.setServletContext(wac.getServletContext()); + assertEquals(1000, resolver.getFileUpload().getSizeMax()); + assertEquals(100, resolver.getFileItemFactory().getSizeThreshold()); + assertEquals("enc", resolver.getFileUpload().getHeaderEncoding()); + assertTrue(resolver.getFileItemFactory().getRepository().getAbsolutePath().endsWith("mytemp")); + + MockHttpServletRequest originalRequest = new MockHttpServletRequest(); + originalRequest.setMethod("POST"); + originalRequest.setContentType("multipart/form-data"); + originalRequest.addHeader("Content-type", "multipart/form-data"); + originalRequest.addParameter("getField", "getValue"); + assertTrue(resolver.isMultipart(originalRequest)); + MultipartHttpServletRequest request = resolver.resolveMultipart(originalRequest); + + Set parameterNames = new HashSet(); + Enumeration parameterEnum = request.getParameterNames(); + while (parameterEnum.hasMoreElements()) { + parameterNames.add(parameterEnum.nextElement()); + } + assertEquals(3, parameterNames.size()); + assertTrue(parameterNames.contains("field3")); + assertTrue(parameterNames.contains("field4")); + assertTrue(parameterNames.contains("getField")); + assertEquals("value3", request.getParameter("field3")); + List parameterValues = Arrays.asList(request.getParameterValues("field3")); + assertEquals(1, parameterValues.size()); + assertTrue(parameterValues.contains("value3")); + assertEquals("value4", request.getParameter("field4")); + parameterValues = Arrays.asList(request.getParameterValues("field4")); + assertEquals(2, parameterValues.size()); + assertTrue(parameterValues.contains("value4")); + assertTrue(parameterValues.contains("value5")); + assertEquals("value4", request.getParameter("field4")); + assertEquals("getValue", request.getParameter("getField")); + + List parameterMapKeys = new ArrayList(); + List parameterMapValues = new ArrayList(); + for (Iterator parameterMapIter = request.getParameterMap().keySet().iterator(); parameterMapIter.hasNext();) { + String key = (String) parameterMapIter.next(); + parameterMapKeys.add(key); + parameterMapValues.add(request.getParameterMap().get(key)); + } + assertEquals(3, parameterMapKeys.size()); + assertEquals(3, parameterMapValues.size()); + int field3Index = parameterMapKeys.indexOf("field3"); + int field4Index = parameterMapKeys.indexOf("field4"); + int getFieldIndex = parameterMapKeys.indexOf("getField"); + assertTrue(field3Index != -1); + assertTrue(field4Index != -1); + assertTrue(getFieldIndex != -1); + parameterValues = Arrays.asList((String[]) parameterMapValues.get(field3Index)); + assertEquals(1, parameterValues.size()); + assertTrue(parameterValues.contains("value3")); + parameterValues = Arrays.asList((String[]) parameterMapValues.get(field4Index)); + assertEquals(2, parameterValues.size()); + assertTrue(parameterValues.contains("value4")); + assertTrue(parameterValues.contains("value5")); + parameterValues = Arrays.asList((String[]) parameterMapValues.get(getFieldIndex)); + assertEquals(1, parameterValues.size()); + assertTrue(parameterValues.contains("getValue")); + + Set fileNames = new HashSet(); + Iterator fileIter = request.getFileNames(); + while (fileIter.hasNext()) { + fileNames.add(fileIter.next()); + } + assertEquals(3, fileNames.size()); + assertTrue(fileNames.contains("field1")); + assertTrue(fileNames.contains("field2")); + assertTrue(fileNames.contains("field2x")); + CommonsMultipartFile file1 = (CommonsMultipartFile) request.getFile("field1"); + CommonsMultipartFile file2 = (CommonsMultipartFile) request.getFile("field2"); + CommonsMultipartFile file2x = (CommonsMultipartFile) request.getFile("field2x"); + Map fileMap = request.getFileMap(); + assertEquals(3, fileMap.size()); + assertTrue(fileMap.containsKey("field1")); + assertTrue(fileMap.containsKey("field2")); + assertTrue(fileMap.containsKey("field2x")); + assertEquals(file1, fileMap.get("field1")); + assertEquals(file2, fileMap.get("field2")); + assertEquals(file2x, fileMap.get("field2x")); + + assertEquals("type1", file1.getContentType()); + assertEquals("type2", file2.getContentType()); + assertEquals("type2", file2x.getContentType()); + assertEquals("field1.txt", file1.getOriginalFilename()); + assertEquals("field2.txt", file2.getOriginalFilename()); + assertEquals("field2x.txt", file2x.getOriginalFilename()); + assertEquals("text1", new String(file1.getBytes())); + assertEquals("text2", new String(file2.getBytes())); + assertEquals(5, file1.getSize()); + assertEquals(5, file2.getSize()); + assertTrue(file1.getInputStream() instanceof ByteArrayInputStream); + assertTrue(file2.getInputStream() instanceof ByteArrayInputStream); + File transfer1 = new File("C:/transfer1"); + File transfer2 = new File("C:/transfer2"); + file1.transferTo(transfer1); + file2.transferTo(transfer2); + assertEquals(transfer1, ((MockFileItem) file1.getFileItem()).writtenFile); + assertEquals(transfer2, ((MockFileItem) file2.getFileItem()).writtenFile); + + MultipartTestBean1 mtb1 = new MultipartTestBean1(); + assertEquals(null, mtb1.getField1()); + assertEquals(null, mtb1.getField2()); + ServletRequestDataBinder binder = new ServletRequestDataBinder(mtb1, "mybean"); + binder.registerCustomEditor(byte[].class, new ByteArrayMultipartFileEditor()); + binder.bind(request); + assertEquals(file1, mtb1.getField1()); + assertEquals(new String(file2.getBytes()), new String(mtb1.getField2())); + + MultipartTestBean2 mtb2 = new MultipartTestBean2(); + assertEquals(null, mtb2.getField1()); + assertEquals(null, mtb2.getField2()); + binder = new ServletRequestDataBinder(mtb2, "mybean"); + binder.registerCustomEditor(String.class, "field1", new StringMultipartFileEditor()); + binder.registerCustomEditor(String.class, "field2", new StringMultipartFileEditor("UTF-16")); + binder.bind(request); + assertEquals(new String(file1.getBytes()), mtb2.getField1()); + assertEquals(new String(file2.getBytes(), "UTF-16"), mtb2.getField2()); + + resolver.cleanupMultipart(request); + assertTrue(((MockFileItem) file1.getFileItem()).deleted); + assertTrue(((MockFileItem) file2.getFileItem()).deleted); + + resolver.setEmpty(true); + request = resolver.resolveMultipart(originalRequest); + binder.setBindEmptyMultipartFiles(false); + String firstBound = mtb2.getField1(); + binder.bind(request); + assertTrue(mtb2.getField1().length() > 0); + assertEquals(firstBound, mtb2.getField1()); + + request = resolver.resolveMultipart(originalRequest); + binder.setBindEmptyMultipartFiles(true); + binder.bind(request); + assertTrue(mtb2.getField1().length() == 0); + } + + public void testWithServletContextAndFilter() throws Exception { + StaticWebApplicationContext wac = new StaticWebApplicationContext(); + wac.setServletContext(new MockServletContext()); + wac.registerSingleton("filterMultipartResolver", MockCommonsMultipartResolver.class, new MutablePropertyValues()); + wac.getServletContext().setAttribute(WebUtils.TEMP_DIR_CONTEXT_ATTRIBUTE, new File("mytemp")); + wac.refresh(); + wac.getServletContext().setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, wac); + CommonsMultipartResolver resolver = new CommonsMultipartResolver(wac.getServletContext()); + assertTrue(resolver.getFileItemFactory().getRepository().getAbsolutePath().endsWith("mytemp")); + + MockFilterConfig filterConfig = new MockFilterConfig(wac.getServletContext(), "filter"); + filterConfig.addInitParameter("class", "notWritable"); + filterConfig.addInitParameter("unknownParam", "someValue"); + final MultipartFilter filter = new MultipartFilter(); + filter.init(filterConfig); + + final List files = new ArrayList(); + final FilterChain filterChain = new FilterChain() { + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse) { + MultipartHttpServletRequest request = (MultipartHttpServletRequest) servletRequest; + files.addAll(request.getFileMap().values()); + } + }; + + FilterChain filterChain2 = new PassThroughFilterChain(filter, filterChain); + + MockHttpServletRequest originalRequest = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + originalRequest.setMethod("POST"); + originalRequest.setContentType("multipart/form-data"); + originalRequest.addHeader("Content-type", "multipart/form-data"); + filter.doFilter(originalRequest, response, filterChain2); + + CommonsMultipartFile file1 = (CommonsMultipartFile) files.get(0); + CommonsMultipartFile file2 = (CommonsMultipartFile) files.get(1); + assertTrue(((MockFileItem) file1.getFileItem()).deleted); + assertTrue(((MockFileItem) file2.getFileItem()).deleted); + } + + public void testWithServletContextAndFilterWithCustomBeanName() throws Exception { + StaticWebApplicationContext wac = new StaticWebApplicationContext(); + wac.setServletContext(new MockServletContext()); + wac.refresh(); + wac.registerSingleton("myMultipartResolver", MockCommonsMultipartResolver.class, new MutablePropertyValues()); + wac.getServletContext().setAttribute(WebUtils.TEMP_DIR_CONTEXT_ATTRIBUTE, new File("mytemp")); + wac.getServletContext().setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, wac); + CommonsMultipartResolver resolver = new CommonsMultipartResolver(wac.getServletContext()); + assertTrue(resolver.getFileItemFactory().getRepository().getAbsolutePath().endsWith("mytemp")); + + MockFilterConfig filterConfig = new MockFilterConfig(wac.getServletContext(), "filter"); + filterConfig.addInitParameter("multipartResolverBeanName", "myMultipartResolver"); + + final List files = new ArrayList(); + FilterChain filterChain = new FilterChain() { + public void doFilter(ServletRequest originalRequest, ServletResponse response) { + if (originalRequest instanceof MultipartHttpServletRequest) { + MultipartHttpServletRequest request = (MultipartHttpServletRequest) originalRequest; + files.addAll(request.getFileMap().values()); + } + } + }; + + MultipartFilter filter = new MultipartFilter() { + private boolean invoked = false; + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + super.doFilterInternal(request, response, filterChain); + super.doFilterInternal(request, response, filterChain); + if (invoked) { + throw new ServletException("Should not have been invoked twice"); + } + invoked = true; + } + }; + filter.init(filterConfig); + + MockHttpServletRequest originalRequest = new MockHttpServletRequest(); + originalRequest.setMethod("POST"); + originalRequest.setContentType("multipart/form-data"); + originalRequest.addHeader("Content-type", "multipart/form-data"); + HttpServletResponse response = new MockHttpServletResponse(); + filter.doFilter(originalRequest, response, filterChain); + CommonsMultipartFile file1 = (CommonsMultipartFile) files.get(0); + CommonsMultipartFile file2 = (CommonsMultipartFile) files.get(1); + assertTrue(((MockFileItem) file1.getFileItem()).deleted); + assertTrue(((MockFileItem) file2.getFileItem()).deleted); + } + + + public static class MockCommonsMultipartResolver extends CommonsMultipartResolver { + + private boolean empty; + + protected void setEmpty(boolean empty) { + this.empty = empty; + } + + protected FileUpload newFileUpload(FileItemFactory fileItemFactory) { + return new ServletFileUpload() { + public List parseRequest(HttpServletRequest request) { + if (request instanceof MultipartHttpServletRequest) { + throw new IllegalStateException("Already a multipart request"); + } + List fileItems = new ArrayList(); + MockFileItem fileItem1 = new MockFileItem( + "field1", "type1", empty ? "" : "field1.txt", empty ? "" : "text1"); + MockFileItem fileItem2 = new MockFileItem( + "field2", "type2", empty ? "" : "C:/field2.txt", empty ? "" : "text2"); + MockFileItem fileItem2x = new MockFileItem( + "field2x", "type2", empty ? "" : "C:\\field2x.txt", empty ? "" : "text2"); + MockFileItem fileItem3 = new MockFileItem("field3", null, null, "value3"); + MockFileItem fileItem4 = new MockFileItem("field4", null, null, "value4"); + MockFileItem fileItem5 = new MockFileItem("field4", null, null, "value5"); + fileItems.add(fileItem1); + fileItems.add(fileItem2); + fileItems.add(fileItem2x); + fileItems.add(fileItem3); + fileItems.add(fileItem4); + fileItems.add(fileItem5); + return fileItems; + } + }; + } + } + + + private static class MockFileItem implements FileItem { + + private String fieldName; + private String contentType; + private String name; + private String value; + + private File writtenFile; + private boolean deleted; + + public MockFileItem(String fieldName, String contentType, String name, String value) { + this.fieldName = fieldName; + this.contentType = contentType; + this.name = name; + this.value = value; + } + + public InputStream getInputStream() throws IOException { + return new ByteArrayInputStream(value.getBytes()); + } + + public String getContentType() { + return contentType; + } + + public String getName() { + return name; + } + + public boolean isInMemory() { + return true; + } + + public long getSize() { + return value.length(); + } + + public byte[] get() { + return value.getBytes(); + } + + public String getString(String encoding) throws UnsupportedEncodingException { + return new String(get(), encoding); + } + + public String getString() { + return value; + } + + public void write(File file) throws Exception { + this.writtenFile = file; + } + + public File getWrittenFile() { + return writtenFile; + } + + public void delete() { + this.deleted = true; + } + + public boolean isDeleted() { + return deleted; + } + + public String getFieldName() { + return fieldName; + } + + public void setFieldName(String s) { + this.fieldName = s; + } + + public boolean isFormField() { + return (this.name == null); + } + + public void setFormField(boolean b) { + throw new UnsupportedOperationException(); + } + + public OutputStream getOutputStream() throws IOException { + throw new UnsupportedOperationException(); + } + } + + + public class MultipartTestBean1 { + + private MultipartFile field1; + private byte[] field2; + + public void setField1(MultipartFile field1) { + this.field1 = field1; + } + + public MultipartFile getField1() { + return field1; + } + + public void setField2(byte[] field2) { + this.field2 = field2; + } + + public byte[] getField2() { + return field2; + } + } + + + public class MultipartTestBean2 { + + private String field1; + private String field2; + + public void setField1(String field1) { + this.field1 = field1; + } + + public String getField1() { + return field1; + } + + public void setField2(String field2) { + this.field2 = field2; + } + + public String getField2() { + return field2; + } + } + +} diff --git a/org.springframework.web/src/test/java/org/springframework/web/multipart/support/ByteArrayMultipartFileEditorTests.java b/org.springframework.web/src/test/java/org/springframework/web/multipart/support/ByteArrayMultipartFileEditorTests.java new file mode 100644 index 00000000000..169f2477e01 --- /dev/null +++ b/org.springframework.web/src/test/java/org/springframework/web/multipart/support/ByteArrayMultipartFileEditorTests.java @@ -0,0 +1,92 @@ +/* + * 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 junit.framework.TestCase; +import org.easymock.MockControl; + +import org.springframework.web.multipart.MultipartFile; + +/** + * @author Rick Evans + */ +public final class ByteArrayMultipartFileEditorTests extends TestCase { + + public void testSetValueAsByteArray() throws Exception { + ByteArrayMultipartFileEditor editor = new ByteArrayMultipartFileEditor(); + String expectedValue = "Shumwere, shumhow, a shuck ish washing you. - Drunken Far Side"; + editor.setValue(expectedValue.getBytes()); + assertEquals(expectedValue, editor.getAsText()); + } + + public void testSetValueAsString() throws Exception { + ByteArrayMultipartFileEditor editor = new ByteArrayMultipartFileEditor(); + String expectedValue = "'Green Wing' - classic British comedy"; + editor.setValue(expectedValue); + assertEquals(expectedValue, editor.getAsText()); + } + + public void testSetValueAsCustomObjectInvokesToString() throws Exception { + ByteArrayMultipartFileEditor editor = new ByteArrayMultipartFileEditor(); + final String expectedValue = "'Green Wing' - classic British comedy"; + Object object = new Object() { + public String toString() { + return expectedValue; + } + }; + editor.setValue(object); + assertEquals(expectedValue, editor.getAsText()); + } + + public void testSetValueAsNullGetsBackEmptyString() throws Exception { + ByteArrayMultipartFileEditor editor = new ByteArrayMultipartFileEditor(); + editor.setValue(null); + assertEquals("", editor.getAsText()); + } + + public void testSetValueAsMultipartFile() throws Exception { + String expectedValue = "That is comforting to know"; + ByteArrayMultipartFileEditor editor = new ByteArrayMultipartFileEditor(); + MockControl mock = MockControl.createControl(MultipartFile.class); + MultipartFile file = (MultipartFile) mock.getMock(); + file.getBytes(); + mock.setReturnValue(expectedValue.getBytes()); + mock.replay(); + editor.setValue(file); + assertEquals(expectedValue, editor.getAsText()); + mock.verify(); + } + + public void testSetValueAsMultipartFileWithBadBytes() throws Exception { + ByteArrayMultipartFileEditor editor = new ByteArrayMultipartFileEditor(); + MockControl mock = MockControl.createControl(MultipartFile.class); + MultipartFile file = (MultipartFile) mock.getMock(); + file.getBytes(); + mock.setThrowable(new IOException()); + mock.replay(); + try { + editor.setValue(file); + fail("Must have thrown an IllegalArgumentException: IOException thrown when reading MultipartFile bytes"); + } + catch (IllegalArgumentException expected) { + } + mock.verify(); + } + +} diff --git a/org.springframework.web/template.mf b/org.springframework.web/template.mf index b638e0798ca..1ab6168973e 100644 --- a/org.springframework.web/template.mf +++ b/org.springframework.web/template.mf @@ -16,6 +16,7 @@ Import-Template: javax.servlet.jsp.*;version="[2.0.0, 3.0.0)";resolution:=optional, javax.xml.*;version="0";resolution:=optional, org.aopalliance.*;version="[1.0.0, 2.0.0)", + org.apache.commons.fileupload.*;version="[1.2.0, 2.0.0)";resolution:=optional, org.apache.commons.httpclient.*;version="[3.1.0, 4.0.0)";resolution:=optional, org.apache.commons.logging.*;version="[1.1.1, 2.0.0)", org.apache.log4j.*;version="[1.2.15, 2.0.0)";resolution:=optional, @@ -23,10 +24,12 @@ Import-Template: org.springframework.beans.*;version="[3.0.0, 3.0.1)", org.springframework.context.*;version="[3.0.0, 3.0.1)", org.springframework.core.*;version="[3.0.0, 3.0.1)", + org.springframework.model.*;version="[3.0.0, 3.0.1)", org.springframework.oxm.*;version="[3.0.0, 3.0.1)";resolution:=optional, org.springframework.remoting.*;version="[3.0.0, 3.0.1)";resolution:=optional, org.springframework.ui.*;version="[3.0.0, 3.0.1)", org.springframework.util.*;version="[3.0.0, 3.0.1)", + org.springframework.validation.*;version="[3.0.0, 3.0.1)", org.xml.sax.*;version="0";resolution:=optional, org.w3c.dom.*;version="0";resolution:=optional Ignored-Existing-Headers: