/* Copyright 2005-2006 Tim Fennell
*
* 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 net.sourceforge.stripes.controller;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import net.sourceforge.stripes.action.FileBean;
import net.sourceforge.stripes.controller.json.JsonContentTypeRequestWrapper;
import net.sourceforge.stripes.controller.multipart.MultipartWrapper;
import net.sourceforge.stripes.exception.StripesServletException;
import net.sourceforge.stripes.exception.UrlBindingConflictException;
/**
* HttpServletRequestWrapper that is used to make the file upload functionality
* transparent as well as request body parameter processing for other content
* types. Every request handled by Stripes is wrapped. Non-default content
* types, such as those containing multipart form file uploads or JSON request
* bodies, are parsed and treated differently, while normal requests are
* silently wrapped and all calls are delegated to the real request.
*
* @author Tim Fennell
* @author Rick Grashel
*/
public class StripesRequestWrapper extends HttpServletRequestWrapper {
/**
* The Multipart Request that parses out all the pieces.
*/
private ContentTypeRequestWrapper contentTypeRequestWrapper;
/**
* The Locale that is going to be used to process the request.
*/
private Locale locale;
/**
* Local copy of the parameter map, into which URI-embedded parameters will
* be merged.
*/
private MergedParameterMap parameterMap;
/**
* Looks for the StripesRequesetWrapper for the specific request and returns
* it. This is done by checking to see if the request is a
* StripesRequestWrapper, and if not, successively unwrapping the request
* until the StripesRequestWrapper is found.
*
* @param request the ServletRequest that is wrapped by a
* StripesRequestWrapper
* @return the StripesRequestWrapper that is wrapping the supplied request
* @throws IllegalStateException if the request is not wrapped by Stripes
*/
public static StripesRequestWrapper findStripesWrapper(ServletRequest request) {
// Loop through any request wrappers looking for the stripes one
while (!(request instanceof StripesRequestWrapper)
&& request != null
&& request instanceof HttpServletRequestWrapper) {
request = ((HttpServletRequestWrapper) request).getRequest();
}
// If we have our wrapper after the loop exits, we're good; otherwise...
if (request instanceof StripesRequestWrapper) {
return (StripesRequestWrapper) request;
} else {
throw new IllegalStateException("A request made it through to some part of Stripes "
+ "without being wrapped in a StripesRequestWrapper. The StripesFilter is "
+ "responsible for wrapping the request, so it is likely that either the "
+ "StripesFilter is not deployed, or that its mappings do not include the "
+ "DispatcherServlet _and_ *.jsp. Stripes does not require that the Stripes "
+ "wrapper is the only request wrapper, or the outermost; only that it is present.");
}
}
/**
* Constructor that will, if the POST is multi-part, parse the POST data and
* make it available through the normal channels. If the request is not a
* multi-part post then it is just wrapped and the behaviour is unchanged.
*
* @param request the HttpServletRequest to wrap this is not a file size
* limit, but a post size limit.
* @throws FileUploadLimitExceededException if the total post size is larger
* than the limit
* @throws StripesServletException if any other error occurs constructing
* the wrapper
*/
public StripesRequestWrapper(HttpServletRequest request) throws StripesServletException {
super(request);
String contentType = request.getContentType();
boolean isPost = "POST".equalsIgnoreCase(request.getMethod());
// Based on the content-type, decide if we need to create content-type
// sensitive request wrappers for parameter handling
if (contentType != null) {
if (isPost && contentType.startsWith("multipart/form-data")) {
constructMultipartWrapper(request);
} else if (contentType.toLowerCase().contains("json") && request.getContentLength() > 0) {
this.contentTypeRequestWrapper = new JsonContentTypeRequestWrapper();
try {
this.contentTypeRequestWrapper.build(request);
} catch (IOException ioe) {
throw new StripesServletException("Could not construct JSON request wrapper.", ioe);
}
}
}
// Create a parameter map that merges the URI parameters with the others
if (contentTypeRequestWrapper != null) {
this.parameterMap = new MergedParameterMap(this, this.contentTypeRequestWrapper);
} else {
this.parameterMap = new MergedParameterMap(this);
}
}
/**
* Responsible for constructing the MultipartWrapper object and setting it
* on to the instance variable 'multipart'.
*
* @param request the HttpServletRequest to wrap this is not a file size
* limit, but a post size limit.
* @throws StripesServletException if any other error occurs constructing
* the wrapper
*/
protected void constructMultipartWrapper(HttpServletRequest request) throws StripesServletException {
try {
this.contentTypeRequestWrapper
= StripesFilter.getConfiguration().getMultipartWrapperFactory().wrap(request);
} catch (Exception e) {
throw new StripesServletException("Could not construct request wrapper.", e);
}
}
/**
* Returns true if this request is wrapping a multipart request, false
* otherwise.
*/
public boolean isMultipart() {
return (this.contentTypeRequestWrapper != null && MultipartWrapper.class.isAssignableFrom(this.contentTypeRequestWrapper.getClass()));
}
/**
* Fetches just the names of regular parameters and does not include file
* upload parameters. If the request is multipart then the information is
* sourced from the parsed multipart object otherwise it is just pulled out
* of the request in the usual manner.
*/
@Override
public Enumeration<String> getParameterNames() {
return Collections.enumeration(getParameterMap().keySet());
}
/**
* Returns all values sent in the request for a given parameter name. If the
* request is multipart then the information is sourced from the parsed
* multipart object otherwise it is just pulled out of the request in the
* usual manner. Values are consistent with
* HttpServletRequest.getParameterValues(String). Values for file uploads
* cannot be retrieved in this way (though parameters sent along with file
* uploads can).
*/
@Override
public String[] getParameterValues(String name) {
/*
* When determining whether to provide a URI parameter's default value, the merged parameter
* map needs to know if the parameter is otherwise defined in the request. It calls this
* method to do that, so if the parameter map is not defined (which it won't be during its
* construction), we delegate to the multipart wrapper or superclass.
*/
MergedParameterMap map = getParameterMap();
if (map == null) {
if (isMultipart()) {
return this.contentTypeRequestWrapper.getParameterValues(name);
} else {
return super.getParameterValues(name);
}
} else {
return map.get(name);
}
}
/**
* Retrieves the first value of the specified parameter from the request. If
* the parameter was not sent, null will be returned.
*/
@Override
public String getParameter(String name) {
String[] values = getParameterValues(name);
if (values != null && values.length > 0) {
return values[0];
} else {
return null;
}
}
/**
* If the request is a clean URL, then extract the parameters from the URI
* and merge with the parameters from the query string and/or request body.
*/
@Override
public MergedParameterMap getParameterMap() {
return this.parameterMap;
}
/**
* Extract new URI parameters from the URI of the given {@code request} and
* merge them with the previous URI parameters.
*/
public void pushUriParameters(HttpServletRequestWrapper request) {
getParameterMap().pushUriParameters(request);
}
/**
* Restore the URI parameters to the state they were in before the previous
* call to {@link #pushUriParameters(HttpServletRequestWrapper)}.
*/
public void popUriParameters() {
getParameterMap().popUriParameters();
}
/**
* Provides access to the Locale being used to process the request.
*
* @return a Locale object representing the chosen locale for the request.
* @see net.sourceforge.stripes.localization.LocalePicker
*/
@Override
public Locale getLocale() {
return locale;
}
/**
* Returns a single element enumeration containing the selected Locale for
* this request.
*
* @see net.sourceforge.stripes.localization.LocalePicker
*/
@Override
public Enumeration<Locale> getLocales() {
List<Locale> list = new ArrayList<Locale>();
list.add(this.locale);
return Collections.enumeration(list);
}
///////////////////////////////////////////////////////////////////////////
// The following methods are specific to the StripesRequestWrapper and are
// not present in the HttpServletRequest interface.
///////////////////////////////////////////////////////////////////////////
/**
* Used by the dispatcher to set the Locale chosen by the configured
* LocalePicker.
*/
protected void setLocale(Locale locale) {
this.locale = locale;
}
/**
* Returns the names of request parameters that represent files being
* uploaded by the user. If no file upload parameters are submitted returns
* an empty enumeration.
*
* @return Returns the file name parameters if this is a multipart-request.
*/
public Enumeration<String> getFileParameterNames() {
if (this.contentTypeRequestWrapper != null && MultipartWrapper.class.isAssignableFrom(this.contentTypeRequestWrapper.getClass())) {
return ((MultipartWrapper) this.contentTypeRequestWrapper).getFileParameterNames();
} else {
return Collections.enumeration(Collections.<String>emptyList());
}
}
/**
* Returns a FileBean representing an uploaded file with the form field name
* = "name". If the form field was present in the request, but no
* file was uploaded, this method will return null.
*
* @param name the form field name of type file
* @return a FileBean if a file was actually submitted by the user,
* otherwise null
*/
public FileBean getFileParameterValue(String name) {
if (this.contentTypeRequestWrapper != null && MultipartWrapper.class.isAssignableFrom(this.contentTypeRequestWrapper.getClass())) {
return ((MultipartWrapper) this.contentTypeRequestWrapper).getFileParameterValue(name);
} else {
return null;
}
}
}
/**
* A {@link Map} implementation that is used by {@link StripesRequestWrapper} to
* merge URI parameter values with request parameter values on the fly.
*
* @author Ben Gunter
*/
class MergedParameterMap implements Map<String, String[]> {
class Entry implements Map.Entry<String, String[]> {
private String key;
Entry(String key) {
this.key = key;
}
public String getKey() {
return key;
}
public String[] getValue() {
return get(key);
}
public String[] setValue(String[] value) {
throw new UnsupportedOperationException();
}
@Override
public boolean equals(Object obj) {
Entry that = (Entry) obj;
return this.key == that.key;
}
@Override
public int hashCode() {
return key.hashCode();
}
@Override
public String toString() {
return "" + key + "=" + Arrays.deepToString(getValue());
}
}
private HttpServletRequestWrapper request;
private Map<String, String[]> uriParams;
private Stack<Map<String, String[]>> uriParamStack;
MergedParameterMap(HttpServletRequestWrapper request) {
this.request = request;
this.uriParams = getUriParameters(request);
if (this.uriParams == null) {
this.uriParams = Collections.emptyMap();
}
}
MergedParameterMap(HttpServletRequestWrapper request, ContentTypeRequestWrapper contentTypeRequestWrapper) {
this.request = request;
// extract URI parameters
Map<String, String[]> uriParams = getUriParameters(request);
/*
* Special handling of parameters if this is a multipart request. The parameters will be
* pulled from the MultipartWrapper and merged in with the URI parameters.
*/
Map<String, String[]> multipartParams = null;
Enumeration<?> names = contentTypeRequestWrapper.getParameterNames();
if (names != null && names.hasMoreElements()) {
multipartParams = new LinkedHashMap<String, String[]>();
while (names.hasMoreElements()) {
String name = (String) names.nextElement();
multipartParams.put(name, contentTypeRequestWrapper.getParameterValues(name));
}
}
// if no multipart params and no URI params then use empty map
if (uriParams == null && multipartParams == null) {
this.uriParams = Collections.emptyMap();
} else {
this.uriParams = mergeParameters(uriParams, multipartParams);
}
}
public void clear() {
throw new UnsupportedOperationException();
}
public boolean containsKey(Object key) {
return getParameterMap().containsKey(key) || uriParams.containsKey(key);
}
public boolean containsValue(Object value) {
return getParameterMap().containsValue(value) || uriParams.containsValue(value);
}
public Set<Map.Entry<String, String[]>> entrySet() {
Set<Map.Entry<String, String[]>> entries = new LinkedHashSet<Map.Entry<String, String[]>>();
for (String key : keySet()) {
entries.add(new Entry(key));
}
return entries;
}
public String[] get(Object key) {
if (key == null) {
return null;
} else {
return mergeParameters(getParameterMap().get(key), uriParams.get(key));
}
}
public boolean isEmpty() {
return getParameterMap().isEmpty() && uriParams.isEmpty();
}
public Set<String> keySet() {
Set<String> merged = new LinkedHashSet<String>();
merged.addAll(uriParams.keySet());
merged.addAll(getParameterMap().keySet());
return merged;
}
public String[] put(String key, String[] value) {
throw new UnsupportedOperationException();
}
public void putAll(Map<? extends String, ? extends String[]> m) {
throw new UnsupportedOperationException();
}
public String[] remove(Object key) {
throw new UnsupportedOperationException();
}
public int size() {
return keySet().size();
}
public Collection<String[]> values() {
Set<String> keys = keySet();
List<String[]> merged = new ArrayList<String[]>(keys.size());
for (String key : keys) {
merged.add(mergeParameters(getParameterMap().get(key), uriParams.get(key)));
}
return merged;
}
@Override
public String toString() {
StringBuilder buf = new StringBuilder("{ ");
for (Map.Entry<String, String[]> entry : entrySet()) {
buf.append(entry).append(", ");
}
if (buf.toString().endsWith(", ")) {
buf.setLength(buf.length() - 2);
}
buf.append(" }");
return buf.toString();
}
/**
* Get the parameter map from the request that is wrapped by the
* {@link StripesRequestWrapper}.
*/
@SuppressWarnings("unchecked")
Map<String, String[]> getParameterMap() {
return request == null ? Collections.<String,String[]>emptyMap() : request.getRequest().getParameterMap();
}
/**
* Extract new URI parameters from the URI of the given {@code request} and
* merge them with the previous URI parameters.
*/
void pushUriParameters(HttpServletRequestWrapper request) {
if (this.uriParamStack == null) {
this.uriParamStack = new Stack<Map<String, String[]>>();
}
Map<String, String[]> map = getUriParameters(request);
this.uriParamStack.push(this.uriParams);
this.uriParams = mergeParameters(new LinkedHashMap<String, String[]>(this.uriParams), map);
}
/**
* Restore the URI parameters to the state they were in before the previous
* call to {@link #pushUriParameters(HttpServletRequestWrapper)}.
*/
void popUriParameters() {
if (this.uriParamStack == null || this.uriParamStack.isEmpty()) {
this.uriParams = null;
} else {
this.uriParams = this.uriParamStack.pop();
}
}
/**
* Extract any parameters embedded in the URI of the given {@code request}
* and return them in a {@link Map}. If no parameters are present in the
* URI, then return null.
*/
Map<String, String[]> getUriParameters(HttpServletRequest request) {
ActionResolver resolver = StripesFilter.getConfiguration().getActionResolver();
if (!(resolver instanceof AnnotatedClassActionResolver)) {
return null;
}
UrlBinding binding = null;
try {
binding = ((AnnotatedClassActionResolver) resolver).getUrlBindingFactory()
.getBinding(request);
} catch (UrlBindingConflictException e) {
// This can be safely ignored
}
Map<String, String[]> params = null;
if (binding != null && binding.getParameters().size() > 0) {
for (UrlBindingParameter p : binding.getParameters()) {
String name = p.getName();
if (name != null) {
String value = p.getValue();
if (UrlBindingParameter.PARAMETER_NAME_EVENT.equals(name)) {
if (value == null) {
// Don't provide the default event name. The dispatcher will handle that
// automatically, and there's no way of knowing at this point if another
// event name is provided by a different parameter.
continue;
} else {
name = value;
value = "";
}
}
if (value == null && request.getParameterValues(name) == null) {
value = p.getDefaultValue();
}
if (value != null) {
if (params == null) {
params = new LinkedHashMap<String, String[]>();
}
String[] values = params.get(name);
if (values == null) {
values = new String[]{value};
} else {
String[] tmp = new String[values.length + 1];
System.arraycopy(values, 0, tmp, 0, values.length);
tmp[tmp.length - 1] = value;
values = tmp;
}
params.put(name, values);
}
}
}
}
return params;
}
/**
* Merge the values from {@code source} into {@code target}.
*/
Map<String, String[]> mergeParameters(Map<String, String[]> target, Map<String, String[]> source) {
// target must not be null and we must not modify source
if (target == null) {
target = new LinkedHashMap<String, String[]>();
}
// nothing to do if source is null or empty
if (source == null || source.isEmpty()) {
return target;
}
// merge the values from source that already exist in target
for (Map.Entry<String, String[]> entry : target.entrySet()) {
entry.setValue(mergeParameters(entry.getValue(), source.get(entry.getKey())));
}
// copy the values from source that do not exist in target
for (Map.Entry<String, String[]> entry : source.entrySet()) {
if (!target.containsKey(entry.getKey())) {
target.put(entry.getKey(), entry.getValue());
}
}
return target;
}
/**
* Merge request parameter values from the original request with the
* parameters that are embedded in the URI. Either or both arguments may be
* empty or null.
*
* @param requestParams the parameters from the original request
* @param uriParams parameters extracted from the URI
* @return the merged parameter values
*/
String[] mergeParameters(String[] requestParams, String[] uriParams) {
if (requestParams == null || requestParams.length == 0) {
if (uriParams == null || uriParams.length == 0) {
return null;
} else {
return uriParams;
}
} else if (uriParams == null || uriParams.length == 0) {
return requestParams;
} else {
String[] merged = new String[uriParams.length + requestParams.length];
System.arraycopy(uriParams, 0, merged, 0, uriParams.length);
System.arraycopy(requestParams, 0, merged, uriParams.length, requestParams.length);
return merged;
}
}
}