001/** 002 * Copyright 2014 Tampere University of Technology, Pori Department 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package core.tut.pori.utils; 017 018import java.io.UnsupportedEncodingException; 019import java.net.URLDecoder; 020import java.net.URLEncoder; 021import java.util.ArrayList; 022import java.util.Arrays; 023import java.util.HashMap; 024import java.util.List; 025import java.util.Map; 026 027import javax.servlet.http.HttpServletRequest; 028 029import org.apache.commons.lang3.ArrayUtils; 030import org.apache.commons.lang3.StringUtils; 031import org.apache.log4j.Logger; 032 033import core.tut.pori.http.Definitions; 034 035/** 036 * common HTTP parameter utility methods 037 * 038 */ 039public final class HTTPParameterUtil { 040 private static final String[] DECODED_CONTENT_TYPES = new String[]{"x-www-form-urlencoded"}; // list of content types already decoded by the servlet container 041 private static final Logger LOGGER = Logger.getLogger(HTTPParameterUtil.class); 042 043 /** 044 * 045 */ 046 private HTTPParameterUtil(){ 047 // nothing needed 048 } 049 050 /** 051 * 052 * <p>separates parameter values based on the defined parameter separator (",")</p> 053 * 054 * <p>This is a helper method for parsing parameter values. By default the map returned by HttpServlerRequest (e.g. getParameterMap()) 055 * will not handle parameter values separated by ",". Also, it will do automatic URL de-coding causing "," and URL encoded comma "%2C" to appear as the same character, making further value separation impossible. 056 * Note that POST body parameters will NOT be separated by "," character because it is impossible to retrieve the raw body parameter list without implementing the entire HTTP request parsing manually through InputStreams.</p> 057 * 058 * <p>e.g. the query string .../method?parameter=value1,value2 will generate a map in which "parameter" is associated with String[] = {"value1,value2"}, and not with String[] = {"value1","value2"}.</p> 059 * 060 * <p>Similarly, the query string .../method?parameter=value1,value2&parameter=value3 will generate a map in which "parameter" is associated with String[] = {"value1,value2","value3"}, and not with String[] = {"value1","value2","value3"}.</p> 061 * 062 * <p>The returned map will contain parameters associated with String[] = {"value1", "value2"} in URL decoded form, preserving the encoded comma (e.g. String[] = {"value1,value2"} if the query string contained ?parameter=value1%2Cvalue2</p> 063 * 064 * <p>This method is uses the HttpServletRequest's parameter map as its basis, and thus, the returned list may contain both URL parameters and url-encoded-form parameters from HTTP body.</p> 065 * 066 * 067 * @param httpServletRequest 068 * @param decode if false, the strings inside the map will NOT be URL decoded 069 * @return the map of parameter or null if none or req was null, note if the parameter has no values, the associated List will be null (NOT empty list) 070 * @throws IllegalArgumentException on bad query string 071 */ 072 public static Map<String, List<String>> getParameterMap(HttpServletRequest httpServletRequest, boolean decode) throws IllegalArgumentException{ 073 if(httpServletRequest == null){ 074 return null; 075 } 076 Map<String, String[]> params = httpServletRequest.getParameterMap(); // the map may contain a mix of body (e.g. form) and URL params 077 if(params == null || params.isEmpty()){ 078 return null; 079 } 080 081 boolean encodeBodyParams = !decode && isDecodedContent(httpServletRequest.getContentType()); // if decode was not requested, but the content was decoded by the container, re-encode it, this is because on some content-types the HttpServletRequest will decode the params even though encoded ones are requested 082 String[] queryStringParams = StringUtils.split(httpServletRequest.getQueryString(), Definitions.SEPARATOR_URI_QUERY_PARAMS); // get parameters strictly appearing in the uri {"param=value","param2=value2"} 083 if(ArrayUtils.isEmpty(queryStringParams)){ 084 Map<String,List<String>> map = new HashMap<>(params.size()); 085 for(Map.Entry<String, String[]> e : params.entrySet()){ // convert arrays to lists, remove unnecessary empty arrays 086 putConverted(map, e.getKey(), e.getValue(), encodeBodyParams); 087 } 088 return map; 089 } 090 091 Map<String, List<String>> queryStringParamMap = new HashMap<>(queryStringParams.length); 092 try { 093 for(int i=0;i<queryStringParams.length;++i){ 094 String[] parts = queryStringParams[i].split(Definitions.SEPARATOR_URI_QUERY_PARAM_VALUE_SEPARATOR); // split the parameters and their values 095 if(parts.length == 2){ 096 List<String> previousValues = queryStringParamMap.get(parts[0]); 097 String[] values = parts[1].split(Definitions.SEPARATOR_URI_QUERY_PARAM_VALUES); // split the values 098 if(previousValues == null){ // create new list if one does not already exist 099 previousValues = new ArrayList<>(values.length); 100 queryStringParamMap.put(parts[0], previousValues); 101 } 102 for(int j=0;j<values.length;++j){ 103 if(decode){ 104 previousValues.add(URLDecoder.decode(values[j], Definitions.ENCODING_UTF8)); 105 }else{ 106 previousValues.add(values[j]); 107 } 108 } 109 }else if(parts.length > 2){ // this is most likely parameter=value=value which is if not entirely wrong, at least slightly ambiguous 110 throw new IllegalArgumentException("Invalid query parameter: "+parts[0]); 111 } // else length == 1, we can ignore this, as in the later loop, this will automatically be replaced with null list 112 } 113 } catch (UnsupportedEncodingException ex) { // this should not happen 114 LOGGER.error(ex, ex); 115 return null; 116 } 117 118 Map<String, List<String>> map = new HashMap<>(params.size()); 119 for(Map.Entry<String, String[]> e : params.entrySet()){ 120 String parameter = e.getKey(); 121 122 List<String> values = queryStringParamMap.get(parameter); 123 if(values == null){ // not an uri parameter, assume it was parsed properly 124 putConverted(map, parameter, e.getValue(), encodeBodyParams); 125 }else{ // it was a query param, in this case discard whatever was in the original map 126 map.put(parameter, values); 127 } 128 } 129 130 return map; 131 } 132 133 /** 134 * 135 * @param contentType 136 * @return true if the given content type denoted decoded content 137 */ 138 private static boolean isDecodedContent(String contentType){ 139 if(StringUtils.isBlank(contentType)){ 140 LOGGER.debug("Content type was null."); 141 return false; 142 } 143 for(int i=0;i<DECODED_CONTENT_TYPES.length;++i){ 144 if(contentType.contains(DECODED_CONTENT_TYPES[i])){ 145 return true; 146 } 147 } 148 return false; 149 } 150 151 /** 152 * helper method for converting the values array to List or setting null if empty 153 * 154 * @param map 155 * @param key 156 * @param values 157 * @param encode if true the values will be URL encoded before applying to the list in the list 158 * @throws IllegalArgumentException 159 */ 160 private static final void putConverted(Map<String, List<String>> map, String key, String[] values, boolean encode) throws IllegalArgumentException{ 161 if(ArrayUtils.isEmpty(values) || (values.length == 1 && StringUtils.isBlank(values[0]))){ 162 map.put(key, null); 163 }else if(encode){ 164 try { 165 ArrayList<String> valueList = new ArrayList<>(values.length); 166 for(int i=0;i<values.length;++i){ 167 valueList.add(URLEncoder.encode(values[i], Definitions.ENCODING_UTF8)); 168 } 169 map.put(key, valueList); 170 } catch (UnsupportedEncodingException ex) { // should never happen 171 LOGGER.error(ex, ex); 172 throw new IllegalArgumentException("Invalid query parameter: "+key); 173 } 174 }else{ 175 map.put(key, Arrays.asList(values)); 176 } 177 } 178}