1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17 package org.apache.commons.fileupload;
18
19 import java.io.UnsupportedEncodingException;
20 import java.util.HashMap;
21 import java.util.Locale;
22 import java.util.Map;
23
24 import org.apache.commons.fileupload.util.mime.MimeUtility;
25
26 /**
27 * A simple parser intended to parse sequences of name/value pairs.
28 *
29 * Parameter values are expected to be enclosed in quotes if they
30 * contain unsafe characters, such as '=' characters or separators.
31 * Parameter values are optional and can be omitted.
32 *
33 * <p>
34 * <code>param1 = value; param2 = "anything goes; really"; param3</code>
35 * </p>
36 */
37 public class ParameterParser {
38
39 /**
40 * String to be parsed.
41 */
42 private char[] chars = null;
43
44 /**
45 * Current position in the string.
46 */
47 private int pos = 0;
48
49 /**
50 * Maximum position in the string.
51 */
52 private int len = 0;
53
54 /**
55 * Start of a token.
56 */
57 private int i1 = 0;
58
59 /**
60 * End of a token.
61 */
62 private int i2 = 0;
63
64 /**
65 * Whether names stored in the map should be converted to lower case.
66 */
67 private boolean lowerCaseNames = false;
68
69 /**
70 * Default ParameterParser constructor.
71 */
72 public ParameterParser() {
73 super();
74 }
75
76 /**
77 * Are there any characters left to parse?
78 *
79 * @return {@code true} if there are unparsed characters,
80 * {@code false} otherwise.
81 */
82 private boolean hasChar() {
83 return this.pos < this.len;
84 }
85
86 /**
87 * A helper method to process the parsed token. This method removes
88 * leading and trailing blanks as well as enclosing quotation marks,
89 * when necessary.
90 *
91 * @param quoted {@code true} if quotation marks are expected,
92 * {@code false} otherwise.
93 * @return the token
94 */
95 private String getToken(boolean quoted) {
96 // Trim leading white spaces
97 while ((i1 < i2) && (Character.isWhitespace(chars[i1]))) {
98 i1++;
99 }
100 // Trim trailing white spaces
101 while ((i2 > i1) && (Character.isWhitespace(chars[i2 - 1]))) {
102 i2--;
103 }
104 // Strip away quotation marks if necessary
105 if (quoted
106 && ((i2 - i1) >= 2)
107 && (chars[i1] == '"')
108 && (chars[i2 - 1] == '"')) {
109 i1++;
110 i2--;
111 }
112 String result = null;
113 if (i2 > i1) {
114 result = new String(chars, i1, i2 - i1);
115 }
116 return result;
117 }
118
119 /**
120 * Tests if the given character is present in the array of characters.
121 *
122 * @param ch the character to test for presense in the array of characters
123 * @param charray the array of characters to test against
124 *
125 * @return {@code true} if the character is present in the array of
126 * characters, {@code false} otherwise.
127 */
128 private boolean isOneOf(char ch, final char[] charray) {
129 boolean result = false;
130 for (char element : charray) {
131 if (ch == element) {
132 result = true;
133 break;
134 }
135 }
136 return result;
137 }
138
139 /**
140 * Parses out a token until any of the given terminators
141 * is encountered.
142 *
143 * @param terminators the array of terminating characters. Any of these
144 * characters when encountered signify the end of the token
145 *
146 * @return the token
147 */
148 private String parseToken(final char[] terminators) {
149 char ch;
150 i1 = pos;
151 i2 = pos;
152 while (hasChar()) {
153 ch = chars[pos];
154 if (isOneOf(ch, terminators)) {
155 break;
156 }
157 i2++;
158 pos++;
159 }
160 return getToken(false);
161 }
162
163 /**
164 * Parses out a token until any of the given terminators
165 * is encountered outside the quotation marks.
166 *
167 * @param terminators the array of terminating characters. Any of these
168 * characters when encountered outside the quotation marks signify the end
169 * of the token
170 *
171 * @return the token
172 */
173 private String parseQuotedToken(final char[] terminators) {
174 char ch;
175 i1 = pos;
176 i2 = pos;
177 boolean quoted = false;
178 boolean charEscaped = false;
179 while (hasChar()) {
180 ch = chars[pos];
181 if (!quoted && isOneOf(ch, terminators)) {
182 break;
183 }
184 if (!charEscaped && ch == '"') {
185 quoted = !quoted;
186 }
187 charEscaped = (!charEscaped && ch == '\\');
188 i2++;
189 pos++;
190
191 }
192 return getToken(true);
193 }
194
195 /**
196 * Returns {@code true} if parameter names are to be converted to lower
197 * case when name/value pairs are parsed.
198 *
199 * @return {@code true} if parameter names are to be
200 * converted to lower case when name/value pairs are parsed.
201 * Otherwise returns {@code false}
202 */
203 public boolean isLowerCaseNames() {
204 return this.lowerCaseNames;
205 }
206
207 /**
208 * Sets the flag if parameter names are to be converted to lower case when
209 * name/value pairs are parsed.
210 *
211 * @param b {@code true} if parameter names are to be
212 * converted to lower case when name/value pairs are parsed.
213 * {@code false} otherwise.
214 */
215 public void setLowerCaseNames(boolean b) {
216 this.lowerCaseNames = b;
217 }
218
219 /**
220 * Extracts a map of name/value pairs from the given string. Names are
221 * expected to be unique. Multiple separators may be specified and
222 * the earliest found in the input string is used.
223 *
224 * @param str the string that contains a sequence of name/value pairs
225 * @param separators the name/value pairs separators
226 *
227 * @return a map of name/value pairs
228 */
229 public Map<String, String> parse(final String str, char[] separators) {
230 if (separators == null || separators.length == 0) {
231 return new HashMap<String, String>();
232 }
233 char separator = separators[0];
234 if (str != null) {
235 int idx = str.length();
236 for (char separator2 : separators) {
237 int tmp = str.indexOf(separator2);
238 if (tmp != -1 && tmp < idx) {
239 idx = tmp;
240 separator = separator2;
241 }
242 }
243 }
244 return parse(str, separator);
245 }
246
247 /**
248 * Extracts a map of name/value pairs from the given string. Names are
249 * expected to be unique.
250 *
251 * @param str the string that contains a sequence of name/value pairs
252 * @param separator the name/value pairs separator
253 *
254 * @return a map of name/value pairs
255 */
256 public Map<String, String> parse(final String str, char separator) {
257 if (str == null) {
258 return new HashMap<String, String>();
259 }
260 return parse(str.toCharArray(), separator);
261 }
262
263 /**
264 * Extracts a map of name/value pairs from the given array of
265 * characters. Names are expected to be unique.
266 *
267 * @param charArray the array of characters that contains a sequence of
268 * name/value pairs
269 * @param separator the name/value pairs separator
270 *
271 * @return a map of name/value pairs
272 */
273 public Map<String, String> parse(final char[] charArray, char separator) {
274 if (charArray == null) {
275 return new HashMap<String, String>();
276 }
277 return parse(charArray, 0, charArray.length, separator);
278 }
279
280 /**
281 * Extracts a map of name/value pairs from the given array of
282 * characters. Names are expected to be unique.
283 *
284 * @param charArray the array of characters that contains a sequence of
285 * name/value pairs
286 * @param offset - the initial offset.
287 * @param length - the length.
288 * @param separator the name/value pairs separator
289 *
290 * @return a map of name/value pairs
291 */
292 public Map<String, String> parse(
293 final char[] charArray,
294 int offset,
295 int length,
296 char separator) {
297
298 if (charArray == null) {
299 return new HashMap<String, String>();
300 }
301 HashMap<String, String> params = new HashMap<String, String>();
302 this.chars = charArray;
303 this.pos = offset;
304 this.len = length;
305
306 String paramName = null;
307 String paramValue = null;
308 while (hasChar()) {
309 paramName = parseToken(new char[] {
310 '=', separator });
311 paramValue = null;
312 if (hasChar() && (charArray[pos] == '=')) {
313 pos++; // skip '='
314 paramValue = parseQuotedToken(new char[] {
315 separator });
316
317 if (paramValue != null) {
318 try {
319 paramValue = MimeUtility.decodeText(paramValue);
320 } catch (UnsupportedEncodingException e) {
321 // let's keep the original value in this case
322 }
323 }
324 }
325 if (hasChar() && (charArray[pos] == separator)) {
326 pos++; // skip separator
327 }
328 if ((paramName != null) && (paramName.length() > 0)) {
329 if (this.lowerCaseNames) {
330 paramName = paramName.toLowerCase(Locale.ENGLISH);
331 }
332
333 params.put(paramName, paramValue);
334 }
335 }
336 return params;
337 }
338
339 }