/*
* Copyright 2004-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 grails.web.servlet.mvc;
import grails.databinding.DataBinder;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.http.HttpServletRequest;
import grails.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import grails.core.GrailsDomainClassProperty;
import org.grails.web.servlet.mvc.GrailsWebRequest;
import org.grails.web.binding.StructuredDateEditor;
import org.grails.web.servlet.mvc.exceptions.ControllerExecutionException;
import grails.web.util.TypeConvertingMap;
import org.grails.web.util.WebUtils;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
/**
* A parameter map class that allows mixing of request parameters and controller parameters. If a controller
* parameter is set with the same name as a request parameter the controller parameter value is retrieved.
*
* @author Graeme Rocher
* @author Lari Hotari
*
* @since Oct 24, 2005
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public class GrailsParameterMap extends TypeConvertingMap implements Cloneable {
private static final Log LOG = LogFactory.getLog(GrailsParameterMap.class);
private static final Map<String, String> CACHED_DATE_FORMATS = new ConcurrentHashMap<String, String>();
private final Map nestedDateMap = new LinkedHashMap();
private final HttpServletRequest request;
public static final String REQUEST_BODY_PARSED = "org.codehaus.groovy.grails.web.REQUEST_BODY_PARSED";
public static final Object[] EMPTY_ARGS = new Object[0];
/**
* Does not populate the GrailsParameterMap from the request but instead uses the supplied values.
*
* @param values The values to populate with
* @param request The request object
*/
public GrailsParameterMap(Map values, HttpServletRequest request) {
this.request = request;
wrappedMap.putAll(values);
}
/**
* Creates a GrailsParameterMap populating from the given request object
* @param request The request object
*/
public GrailsParameterMap(HttpServletRequest request) {
this.request = request;
final Map requestMap = new LinkedHashMap(request.getParameterMap());
if (requestMap.isEmpty() && ("PUT".equals(request.getMethod()) || "PATCH".equals(request.getMethod())) && request.getAttribute(REQUEST_BODY_PARSED) == null) {
// attempt manual parse of request body. This is here because some containers don't parse the request body automatically for PUT request
String contentType = request.getContentType();
if ("application/x-www-form-urlencoded".equals(contentType)) {
try {
Reader reader = request.getReader();
if(reader != null) {
String contents = IOUtils.toString(reader);
request.setAttribute(REQUEST_BODY_PARSED, true);
requestMap.putAll(WebUtils.fromQueryString(contents));
}
} catch (Exception e) {
LOG.error("Error processing form encoded " + request.getMethod() + " request", e);
}
}
}
if (request instanceof MultipartHttpServletRequest) {
Map<String,MultipartFile> fileMap = ((MultipartHttpServletRequest)request).getFileMap();
for (Map.Entry<String, MultipartFile> entry : fileMap.entrySet()) {
requestMap.put(entry.getKey(), entry.getValue());
}
}
updateNestedKeys(requestMap);
}
@Override
public Object clone() {
if (wrappedMap.isEmpty()) {
return new GrailsParameterMap(new LinkedHashMap(), request);
} else {
Map clonedMap = new LinkedHashMap(wrappedMap);
// deep clone nested entries
for(Iterator it=clonedMap.entrySet().iterator();it.hasNext();) {
Map.Entry entry = (Map.Entry)it.next();
if (entry.getValue() instanceof GrailsParameterMap) {
entry.setValue(((GrailsParameterMap)entry.getValue()).clone());
}
}
return new GrailsParameterMap(clonedMap, request);
}
}
public void addParametersFrom(GrailsParameterMap otherMap) {
wrappedMap.putAll((GrailsParameterMap)otherMap.clone());
}
/**
* @return Returns the request.
*/
public HttpServletRequest getRequest() {
return request;
}
@Override
public Object get(Object key) {
// removed test for String key because there
// should be no limitations on what you shove in or take out
Object returnValue = null;
if (nestedDateMap.containsKey(key)) {
returnValue = nestedDateMap.get(key);
} else {
returnValue = wrappedMap.get(key);
if (returnValue instanceof String[]) {
String[] valueArray = (String[])returnValue;
if (valueArray.length == 1) {
returnValue = valueArray[0];
} else {
returnValue = valueArray;
}
}
else if(returnValue == null && (key instanceof Collection)) {
return DefaultGroovyMethods.subMap(wrappedMap, (Collection)key);
}
}
if ("date.struct".equals(returnValue)) {
returnValue = lazyEvaluateDateParam(key);
nestedDateMap.put(key, returnValue);
}
return returnValue;
}
@Override
public Object put(Object key, Object value) {
if (value instanceof CharSequence) value = value.toString();
if (key instanceof CharSequence) key = key.toString();
if (nestedDateMap.containsKey(key)) nestedDateMap.remove(key);
Object returnValue = wrappedMap.put(key, value);
if (key instanceof String) {
String keyString = (String)key;
if (keyString.indexOf(".") > -1) {
processNestedKeys(this, keyString, keyString, wrappedMap);
}
}
return returnValue;
}
@Override
public Object remove(Object key) {
nestedDateMap.remove(key);
return wrappedMap.remove(key);
}
@Override
public void putAll(Map map) {
for (Object entryObj : map.entrySet()) {
Map.Entry entry = (Map.Entry)entryObj;
put(entry.getKey(), entry.getValue());
}
}
/**
* Obtains a date for the parameter name using the default format
*
* @param name The name of the parameter
* @return A date or null
*/
@Override
public Date getDate(String name) {
Date date = super.getDate(name);
if (date == null) {
// try lookup format from messages.properties
String format = lookupFormat(name);
if (format != null) {
return getDate(name, format);
}
}
return date;
}
/**
* Converts this parameter map into a query String. Note that this will flatten nested keys separating them with the
* . character and URL encode the result
*
* @return A query String starting with the ? character
*/
public String toQueryString() {
String encoding = request.getCharacterEncoding();
try {
return WebUtils.toQueryString(this, encoding);
}
catch (UnsupportedEncodingException e) {
throw new ControllerExecutionException("Unable to convert parameter map [" + this +
"] to a query string: " + e.getMessage(), e);
}
}
/**
* @return The identifier in the request
*/
public Object getIdentifier() {
return get(GrailsDomainClassProperty.IDENTITY);
}
private String lookupFormat(String name) {
String format = CACHED_DATE_FORMATS.get(name);
if (format == null) {
GrailsWebRequest webRequest = GrailsWebRequest.lookup(request);
if (webRequest != null) {
MessageSource messageSource = webRequest.getApplicationContext();
if (messageSource != null) {
format = messageSource.getMessage("date." + name + ".format", EMPTY_ARGS, webRequest.getLocale());
if (format != null) {
CACHED_DATE_FORMATS.put(name, format);
}
}
}
}
return format;
}
protected void updateNestedKeys(Map keys) {
for (Object keyObject : keys.keySet()) {
String key = (String)keyObject;
Object paramValue = getParameterValue(keys, key);
wrappedMap.put(key, paramValue);
processNestedKeys(keys, key, key, wrappedMap);
}
}
private Date lazyEvaluateDateParam(Object key) {
// parse date structs automatically
Map dateParams = new LinkedHashMap();
for (Object entryObj : entrySet()) {
Map.Entry entry = (Map.Entry)entryObj;
Object entryKey = entry.getKey();
if (entryKey instanceof String) {
String paramName = (String)entryKey;
final String prefix = key + "_";
if (paramName.startsWith(prefix)) {
dateParams.put(paramName.substring(prefix.length(), paramName.length()), entry.getValue());
}
}
}
DateFormat dateFormat = new SimpleDateFormat(DataBinder.DEFAULT_DATE_FORMAT,
LocaleContextHolder.getLocale());
StructuredDateEditor editor = new StructuredDateEditor(dateFormat, true);
try {
return (Date)editor.assemble(Date.class, dateParams);
}
catch (IllegalArgumentException e) {
return null;
}
}
private Object getParameterValue(Map requestMap, String key) {
Object paramValue = requestMap.get(key);
if (paramValue instanceof String[]) {
if (((String[])paramValue).length == 1) {
paramValue = ((String[])paramValue)[0];
}
}
return paramValue;
}
/*
* Builds up a multi dimensional hash structure from the parameters so that nested keys such as
* "book.author.name" can be addressed like params['author'].name
*
* This also allows data binding to occur for only a subset of the properties in the parameter map.
*/
private void processNestedKeys(Map requestMap, String key, String nestedKey, Map nestedLevel) {
final int nestedIndex = nestedKey.indexOf('.');
if (nestedIndex == -1) {
return;
}
// We have at least one sub-key, so extract the first element
// of the nested key as the prfix. In other words, if we have
// 'nestedKey' == "a.b.c", the prefix is "a".
String nestedPrefix = nestedKey.substring(0, nestedIndex);
boolean prefixedByUnderscore = false;
// Use the same prefix even if it starts with an '_'
if (nestedPrefix.startsWith("_")) {
prefixedByUnderscore = true;
nestedPrefix = nestedPrefix.substring(1);
}
// Let's see if we already have a value in the current map for the prefix.
Object prefixValue = nestedLevel.get(nestedPrefix);
if (prefixValue == null) {
// No value. So, since there is at least one sub-key,
// we create a sub-map for this prefix.
prefixValue = new GrailsParameterMap(new LinkedHashMap(), request);
nestedLevel.put(nestedPrefix, prefixValue);
}
// If the value against the prefix is a map, then we store the sub-keys in that map.
if (!(prefixValue instanceof Map)) {
return;
}
Map nestedMap = (Map)prefixValue;
if (nestedIndex < nestedKey.length() - 1) {
String remainderOfKey = nestedKey.substring(nestedIndex + 1, nestedKey.length());
// GRAILS-2486 Cascade the '_' prefix in order to bind checkboxes properly
if (prefixedByUnderscore) {
remainderOfKey = '_' + remainderOfKey;
}
nestedMap.put(remainderOfKey,getParameterValue(requestMap, key));
if (!(nestedMap instanceof GrailsParameterMap) && remainderOfKey.indexOf('.') >-1) {
processNestedKeys(requestMap, remainderOfKey, remainderOfKey, nestedMap);
}
}
}
}