001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.fileupload; 018 019import java.io.UnsupportedEncodingException; 020import java.util.HashMap; 021import java.util.Locale; 022import java.util.Map; 023 024import org.apache.commons.fileupload.util.mime.MimeUtility; 025 026/** 027 * A simple parser intended to parse sequences of name/value pairs. 028 * 029 * Parameter values are expected to be enclosed in quotes if they 030 * contain unsafe characters, such as '=' characters or separators. 031 * Parameter values are optional and can be omitted. 032 * 033 * <p> 034 * {@code param1 = value; param2 = "anything goes; really"; param3} 035 * </p> 036 */ 037public class ParameterParser { 038 039 /** 040 * String to be parsed. 041 */ 042 private char[] chars; 043 044 /** 045 * Current position in the string. 046 */ 047 private int pos; 048 049 /** 050 * Maximum position in the string. 051 */ 052 private int len; 053 054 /** 055 * Start of a token. 056 */ 057 private int i1; 058 059 /** 060 * End of a token. 061 */ 062 private int i2; 063 064 /** 065 * Whether names stored in the map should be converted to lower case. 066 */ 067 private boolean lowerCaseNames; 068 069 /** 070 * Default ParameterParser constructor. 071 */ 072 public ParameterParser() { 073 } 074 075 /** 076 * A helper method to process the parsed token. This method removes 077 * leading and trailing blanks as well as enclosing quotation marks, 078 * when necessary. 079 * 080 * @param quoted {@code true} if quotation marks are expected, 081 * {@code false} otherwise. 082 * @return the token 083 */ 084 private String getToken(final boolean quoted) { 085 // Trim leading white spaces 086 while (i1 < i2 && Character.isWhitespace(chars[i1])) { 087 i1++; 088 } 089 // Trim trailing white spaces 090 while (i2 > i1 && Character.isWhitespace(chars[i2 - 1])) { 091 i2--; 092 } 093 // Strip away quotation marks if necessary 094 if (quoted 095 && i2 - i1 >= 2 096 && chars[i1] == '"' 097 && chars[i2 - 1] == '"') { 098 i1++; 099 i2--; 100 } 101 String result = null; 102 if (i2 > i1) { 103 result = new String(chars, i1, i2 - i1); 104 } 105 return result; 106 } 107 108 /** 109 * Are there any characters left to parse? 110 * 111 * @return {@code true} if there are unparsed characters, 112 * {@code false} otherwise. 113 */ 114 private boolean hasChar() { 115 return pos < len; 116 } 117 118 /** 119 * Returns {@code true} if parameter names are to be converted to lower 120 * case when name/value pairs are parsed. 121 * 122 * @return {@code true} if parameter names are to be 123 * converted to lower case when name/value pairs are parsed. 124 * Otherwise returns {@code false} 125 */ 126 public boolean isLowerCaseNames() { 127 return lowerCaseNames; 128 } 129 130 /** 131 * Tests if the given character is present in the array of characters. 132 * 133 * @param ch the character to test for presence in the array of characters 134 * @param charray the array of characters to test against 135 * @return {@code true} if the character is present in the array of 136 * characters, {@code false} otherwise. 137 */ 138 private boolean isOneOf(final char ch, final char[] charray) { 139 boolean result = false; 140 for (final char element : charray) { 141 if (ch == element) { 142 result = true; 143 break; 144 } 145 } 146 return result; 147 } 148 149 /** 150 * Extracts a map of name/value pairs from the given array of 151 * characters. Names are expected to be unique. 152 * 153 * @param charArray the array of characters that contains a sequence of 154 * name/value pairs 155 * @param separator the name/value pairs separator 156 * @return a map of name/value pairs 157 */ 158 public Map<String, String> parse(final char[] charArray, final char separator) { 159 if (charArray == null) { 160 return new HashMap<>(); 161 } 162 return parse(charArray, 0, charArray.length, separator); 163 } 164 165 /** 166 * Extracts a map of name/value pairs from the given array of characters. Names are expected to be unique. 167 * 168 * @param charArray the array of characters that contains a sequence of name/value pairs 169 * @param offset the initial offset. 170 * @param length the length. 171 * @param separator the name/value pairs separator 172 * @return a map of name/value pairs 173 */ 174 public Map<String, String> parse(final char[] charArray, final int offset, final int length, final char separator) { 175 if (charArray == null) { 176 return new HashMap<>(); 177 } 178 final HashMap<String, String> params = new HashMap<>(); 179 chars = charArray.clone(); 180 pos = offset; 181 len = length; 182 while (hasChar()) { 183 String paramName = parseToken(new char[] { '=', separator }); 184 String paramValue = null; 185 if (hasChar() && charArray[pos] == '=') { 186 pos++; // skip '=' 187 paramValue = parseQuotedToken(new char[] { separator }); 188 if (paramValue != null) { 189 try { 190 paramValue = RFC2231Utility.hasEncodedValue(paramName) ? RFC2231Utility.decodeText(paramValue) : MimeUtility.decodeText(paramValue); 191 } catch (final UnsupportedEncodingException e) { 192 // let's keep the original value in this case 193 } 194 } 195 } 196 if (hasChar() && charArray[pos] == separator) { 197 pos++; // skip separator 198 } 199 if (paramName != null && !paramName.isEmpty()) { 200 paramName = RFC2231Utility.stripDelimiter(paramName); 201 if (lowerCaseNames) { 202 paramName = paramName.toLowerCase(Locale.ROOT); 203 } 204 params.put(paramName, paramValue); 205 } 206 } 207 return params; 208 } 209 210 /** 211 * Extracts a map of name/value pairs from the given string. Names are 212 * expected to be unique. 213 * 214 * @param str the string that contains a sequence of name/value pairs 215 * @param separator the name/value pairs separator 216 * @return a map of name/value pairs 217 */ 218 public Map<String, String> parse(final String str, final char separator) { 219 if (str == null) { 220 return new HashMap<>(); 221 } 222 return parse(str.toCharArray(), separator); 223 } 224 225 /** 226 * Extracts a map of name/value pairs from the given string. Names are 227 * expected to be unique. Multiple separators may be specified and 228 * the earliest found in the input string is used. 229 * 230 * @param str the string that contains a sequence of name/value pairs 231 * @param separators the name/value pairs separators 232 * @return a map of name/value pairs 233 */ 234 public Map<String, String> parse(final String str, final char[] separators) { 235 if (separators == null || separators.length == 0) { 236 return new HashMap<>(); 237 } 238 char separator = separators[0]; 239 if (str != null) { 240 int idx = str.length(); 241 for (final char separator2 : separators) { 242 final int tmp = str.indexOf(separator2); 243 if (tmp != -1 && tmp < idx) { 244 idx = tmp; 245 separator = separator2; 246 } 247 } 248 } 249 return parse(str, separator); 250 } 251 252 /** 253 * Parses out a token until any of the given terminators 254 * is encountered outside the quotation marks. 255 * 256 * @param terminators the array of terminating characters. Any of these 257 * characters when encountered outside the quotation marks signify the end 258 * of the token 259 * 260 * @return the token 261 */ 262 private String parseQuotedToken(final char[] terminators) { 263 char ch; 264 i1 = pos; 265 i2 = pos; 266 boolean quoted = false; 267 boolean charEscaped = false; 268 while (hasChar()) { 269 ch = chars[pos]; 270 if (!quoted && isOneOf(ch, terminators)) { 271 break; 272 } 273 if (!charEscaped && ch == '"') { 274 quoted = !quoted; 275 } 276 charEscaped = !charEscaped && ch == '\\'; 277 i2++; 278 pos++; 279 280 } 281 return getToken(true); 282 } 283 284 /** 285 * Parses out a token until any of the given terminators 286 * is encountered. 287 * 288 * @param terminators the array of terminating characters. Any of these 289 * characters when encountered signify the end of the token 290 * 291 * @return the token 292 */ 293 private String parseToken(final char[] terminators) { 294 char ch; 295 i1 = pos; 296 i2 = pos; 297 while (hasChar()) { 298 ch = chars[pos]; 299 if (isOneOf(ch, terminators)) { 300 break; 301 } 302 i2++; 303 pos++; 304 } 305 return getToken(false); 306 } 307 308 /** 309 * Sets the flag if parameter names are to be converted to lower case when 310 * name/value pairs are parsed. 311 * 312 * @param b {@code true} if parameter names are to be 313 * converted to lower case when name/value pairs are parsed. 314 * {@code false} otherwise. 315 */ 316 public void setLowerCaseNames(final boolean b) { 317 lowerCaseNames = b; 318 } 319 320}