View Javadoc
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}
35   * </p>
36   */
37  public class ParameterParser {
38  
39      /**
40       * String to be parsed.
41       */
42      private char[] chars;
43  
44      /**
45       * Current position in the string.
46       */
47      private int pos;
48  
49      /**
50       * Maximum position in the string.
51       */
52      private int len;
53  
54      /**
55       * Start of a token.
56       */
57      private int i1;
58  
59      /**
60       * End of a token.
61       */
62      private int i2;
63  
64      /**
65       * Whether names stored in the map should be converted to lower case.
66       */
67      private boolean lowerCaseNames;
68  
69      /**
70       * Default ParameterParser constructor.
71       */
72      public ParameterParser() {
73      }
74  
75      /**
76       * A helper method to process the parsed token. This method removes
77       * leading and trailing blanks as well as enclosing quotation marks,
78       * when necessary.
79       *
80       * @param quoted {@code true} if quotation marks are expected,
81       *               {@code false} otherwise.
82       * @return the token
83       */
84      private String getToken(final boolean quoted) {
85          // Trim leading white spaces
86          while (i1 < i2 && Character.isWhitespace(chars[i1])) {
87              i1++;
88          }
89          // Trim trailing white spaces
90          while (i2 > i1 && Character.isWhitespace(chars[i2 - 1])) {
91              i2--;
92          }
93          // Strip away quotation marks if necessary
94          if (quoted
95              && i2 - i1 >= 2
96              && chars[i1] == '"'
97              && chars[i2 - 1] == '"') {
98              i1++;
99              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 }