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&amp;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}