Retry transactions in the face of optimistic locking exceptions for all "write" trans...
[pithos] / src / gr / ebs / gss / server / webdav / RequestUtil.java
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 gr.ebs.gss.server.webdav;
18
19 import java.io.UnsupportedEncodingException;
20 import java.text.SimpleDateFormat;
21 import java.util.Map;
22 import java.util.TimeZone;
23
24
25 /**
26  * General purpose request parsing and encoding utility methods.
27  *
28  * @author Craig R. McClanahan
29  * @author Tim Tye
30  * @version $Revision$ $Date$
31  */
32
33 public final class RequestUtil {
34
35
36     /**
37      * The DateFormat to use for generating readable dates in cookies.
38      */
39     private static SimpleDateFormat format =
40         new SimpleDateFormat(" EEEE, dd-MMM-yy kk:mm:ss zz");
41
42     static {
43         format.setTimeZone(TimeZone.getTimeZone("GMT"));
44     }
45
46
47     /**
48      * Filter the specified message string for characters that are sensitive
49      * in HTML.  This avoids potential attacks caused by including JavaScript
50      * codes in the request URL that is often reported in error messages.
51      *
52      * @param message The message string to be filtered
53      * @return the filtered string
54      */
55     public static String filter(String message) {
56
57         if (message == null)
58             return null;
59
60         char content[] = new char[message.length()];
61         message.getChars(0, message.length(), content, 0);
62         StringBuffer result = new StringBuffer(content.length + 50);
63         for (int i = 0; i < content.length; i++)
64                         switch (content[i]) {
65             case '<':
66                 result.append("&lt;");
67                 break;
68             case '>':
69                 result.append("&gt;");
70                 break;
71             case '&':
72                 result.append("&amp;");
73                 break;
74             case '"':
75                 result.append("&quot;");
76                 break;
77             default:
78                 result.append(content[i]);
79             }
80         return result.toString();
81
82     }
83
84
85     /**
86      * Normalize a relative URI path that may have relative values ("/./",
87      * "/../", and so on ) it it.  <strong>WARNING</strong> - This method is
88      * useful only for normalizing application-generated paths.  It does not
89      * try to perform security checks for malicious input.
90      *
91      * @param path Relative path to be normalized
92      * @return the normalized string
93      */
94     public static String normalize(String path) {
95         return normalize(path, true);
96         }
97
98     /**
99      * Normalize a relative URI path that may have relative values ("/./",
100      * "/../", and so on ) it it.  <strong>WARNING</strong> - This method is
101      * useful only for normalizing application-generated paths.  It does not
102      * try to perform security checks for malicious input.
103      *
104      * @param path Relative path to be normalized
105      * @param replaceBackSlash Should '\\' be replaced with '/'
106      */
107     public static String normalize(String path, boolean replaceBackSlash) {
108
109         if (path == null)
110             return null;
111
112         // Create a place for the normalized path
113         String normalized = path;
114
115         if (replaceBackSlash && normalized.indexOf('\\') >= 0)
116                 normalized = normalized.replace('\\', '/');
117
118         if (normalized.equals("/."))
119             return "/";
120
121         // Add a leading "/" if necessary
122         if (!normalized.startsWith("/"))
123             normalized = "/" + normalized;
124
125         // Resolve occurrences of "//" in the normalized path
126         while (true) {
127             int index = normalized.indexOf("//");
128             if (index < 0)
129                 break;
130             normalized = normalized.substring(0, index) +
131                 normalized.substring(index + 1);
132         }
133
134         // Resolve occurrences of "/./" in the normalized path
135         while (true) {
136             int index = normalized.indexOf("/./");
137             if (index < 0)
138                 break;
139             normalized = normalized.substring(0, index) +
140                 normalized.substring(index + 2);
141         }
142
143         // Resolve occurrences of "/../" in the normalized path
144         while (true) {
145             int index = normalized.indexOf("/../");
146             if (index < 0)
147                 break;
148             if (index == 0)
149                 return null;  // Trying to go outside our context
150             int index2 = normalized.lastIndexOf('/', index - 1);
151             normalized = normalized.substring(0, index2) +
152                 normalized.substring(index + 3);
153         }
154
155         // Return the normalized path that we have completed
156         return normalized;
157
158     }
159
160
161     /**
162      * Append request parameters from the specified String to the specified
163      * Map.  It is presumed that the specified Map is not accessed from any
164      * other thread, so no synchronization is performed.
165      * <p>
166      * <strong>IMPLEMENTATION NOTE</strong>:  URL decoding is performed
167      * individually on the parsed name and value elements, rather than on
168      * the entire query string ahead of time, to properly deal with the case
169      * where the name or value includes an encoded "=" or "&" character
170      * that would otherwise be interpreted as a delimiter.
171      *
172      * @param map Map that accumulates the resulting parameters
173      * @param data Input string containing request parameters
174      * @param encoding
175      * @throws UnsupportedEncodingException
176      *
177      * @exception IllegalArgumentException if the data is malformed
178      */
179     public static void parseParameters(Map map, String data, String encoding)
180         throws UnsupportedEncodingException {
181
182         if (data != null && data.length() > 0) {
183
184             // use the specified encoding to extract bytes out of the
185             // given string so that the encoding is not lost. If an
186             // encoding is not specified, let it use platform default
187             byte[] bytes = null;
188             try {
189                 if (encoding == null)
190                                         bytes = data.getBytes();
191                                 else
192                                         bytes = data.getBytes(encoding);
193             } catch (UnsupportedEncodingException uee) {
194             }
195
196             parseParameters(map, bytes, encoding);
197         }
198
199     }
200
201
202     /**
203      * Decode and return the specified URL-encoded String.
204      * When the byte array is converted to a string, the system default
205      * character encoding is used...  This may be different than some other
206      * servers. It is assumed the string is not a query string.
207      *
208      * @param str The url-encoded string
209      * @return the decoded string
210      *
211      * @exception IllegalArgumentException if a '%' character is not followed
212      * by a valid 2-digit hexadecimal number
213      */
214     public static String URLDecode(String str) {
215         return URLDecode(str, null);
216     }
217
218
219     /**
220      * Decode and return the specified URL-encoded String. It is assumed the
221      * string is not a query string.
222      *
223      * @param str The url-encoded string
224      * @param enc The encoding to use; if null, the default encoding is used
225      * @return the decoded string
226      * @exception IllegalArgumentException if a '%' character is not followed
227      * by a valid 2-digit hexadecimal number
228      */
229     public static String URLDecode(String str, String enc) {
230         return URLDecode(str, enc, false);
231     }
232
233     /**
234      * Decode and return the specified URL-encoded String.
235      *
236      * @param str The url-encoded string
237      * @param enc The encoding to use; if null, the default encoding is used
238      * @param isQuery Is this a query string being processed
239      * @return the decoded string
240      * @exception IllegalArgumentException if a '%' character is not followed
241      * by a valid 2-digit hexadecimal number
242      */
243     public static String URLDecode(String str, String enc, boolean isQuery) {
244         if (str == null)
245             return null;
246
247         // use the specified encoding to extract bytes out of the
248         // given string so that the encoding is not lost. If an
249         // encoding is not specified, let it use platform default
250         byte[] bytes = null;
251         try {
252             if (enc == null)
253                                 bytes = str.getBytes();
254                         else
255                                 bytes = str.getBytes(enc);
256         } catch (UnsupportedEncodingException uee) {}
257
258         return URLDecode(bytes, enc, isQuery);
259
260     }
261
262
263     /**
264      * Decode and return the specified URL-encoded byte array. It is assumed
265      * the string is not a query string.
266      *
267      * @param bytes The url-encoded byte array
268      * @return the decoded string
269      * @exception IllegalArgumentException if a '%' character is not followed
270      * by a valid 2-digit hexadecimal number
271      */
272     public static String URLDecode(byte[] bytes) {
273         return URLDecode(bytes, null);
274     }
275
276
277     /**
278      * Decode and return the specified URL-encoded byte array. It is assumed
279      * the string is not a query string.
280      *
281      * @param bytes The url-encoded byte array
282      * @param enc The encoding to use; if null, the default encoding is used
283      * @return the decoded string
284      * @exception IllegalArgumentException if a '%' character is not followed
285      * by a valid 2-digit hexadecimal number
286      */
287     public static String URLDecode(byte[] bytes, String enc) {
288         return URLDecode(bytes, null, false);
289     }
290
291     /**
292      * Decode and return the specified URL-encoded byte array.
293      *
294      * @param bytes The url-encoded byte array
295      * @param enc The encoding to use; if null, the default encoding is used
296      * @param isQuery Is this a query string being processed
297      * @return the decoded string
298      * @exception IllegalArgumentException if a '%' character is not followed
299      * by a valid 2-digit hexadecimal number
300      */
301     public static String URLDecode(byte[] bytes, String enc, boolean isQuery) {
302
303         if (bytes == null)
304             return null;
305
306         int len = bytes.length;
307         int ix = 0;
308         int ox = 0;
309         while (ix < len) {
310             byte b = bytes[ix++];     // Get byte to test
311             if (b == '+' && isQuery)
312                                 b = (byte)' ';
313                         else if (b == '%')
314                                 b = (byte) ((convertHexDigit(bytes[ix++]) << 4)
315                             + convertHexDigit(bytes[ix++]));
316             bytes[ox++] = b;
317         }
318         if (enc != null)
319                         try {
320                 return new String(bytes, 0, ox, enc);
321             } catch (Exception e) {
322                 e.printStackTrace();
323             }
324         return new String(bytes, 0, ox);
325
326     }
327
328
329     /**
330      * Convert a byte character value to hexidecimal digit value.
331      *
332      * @param b the character value byte
333      * @return the converted value
334      */
335     private static byte convertHexDigit( byte b ) {
336         if (b >= '0' && b <= '9') return (byte)(b - '0');
337         if (b >= 'a' && b <= 'f') return (byte)(b - 'a' + 10);
338         if (b >= 'A' && b <= 'F') return (byte)(b - 'A' + 10);
339         return 0;
340     }
341
342
343     /**
344      * Put name and value pair in map.  When name already exist, add value
345      * to array of values.
346      *
347      * @param map The map to populate
348      * @param name The parameter name
349      * @param value The parameter value
350      */
351     private static void putMapEntry( Map map, String name, String value) {
352         String[] newValues = null;
353         String[] oldValues = (String[]) map.get(name);
354         if (oldValues == null) {
355             newValues = new String[1];
356             newValues[0] = value;
357         } else {
358             newValues = new String[oldValues.length + 1];
359             System.arraycopy(oldValues, 0, newValues, 0, oldValues.length);
360             newValues[oldValues.length] = value;
361         }
362         map.put(name, newValues);
363     }
364
365
366     /**
367      * Append request parameters from the specified String to the specified
368      * Map.  It is presumed that the specified Map is not accessed from any
369      * other thread, so no synchronization is performed.
370      * <p>
371      * <strong>IMPLEMENTATION NOTE</strong>:  URL decoding is performed
372      * individually on the parsed name and value elements, rather than on
373      * the entire query string ahead of time, to properly deal with the case
374      * where the name or value includes an encoded "=" or "&" character
375      * that would otherwise be interpreted as a delimiter.
376      *
377      * NOTE: byte array data is modified by this method.  Caller beware.
378      *
379      * @param map Map that accumulates the resulting parameters
380      * @param data Input string containing request parameters
381      * @param encoding Encoding to use for converting hex
382      *
383      * @exception UnsupportedEncodingException if the data is malformed
384      */
385     public static void parseParameters(Map map, byte[] data, String encoding)
386         throws UnsupportedEncodingException {
387
388         if (data != null && data.length > 0) {
389             int    ix = 0;
390             int    ox = 0;
391             String key = null;
392             String value = null;
393             while (ix < data.length) {
394                 byte c = data[ix++];
395                 switch ((char) c) {
396                 case '&':
397                     value = new String(data, 0, ox, encoding);
398                     if (key != null) {
399                         putMapEntry(map, key, value);
400                         key = null;
401                     }
402                     ox = 0;
403                     break;
404                 case '=':
405                     if (key == null) {
406                         key = new String(data, 0, ox, encoding);
407                         ox = 0;
408                     } else
409                                                 data[ox++] = c;
410                     break;
411                 case '+':
412                     data[ox++] = (byte)' ';
413                     break;
414                 case '%':
415                     data[ox++] = (byte)((convertHexDigit(data[ix++]) << 4)
416                                     + convertHexDigit(data[ix++]));
417                     break;
418                 default:
419                     data[ox++] = c;
420                 }
421             }
422             //The last value does not end in '&'.  So save it now.
423             if (key != null) {
424                 value = new String(data, 0, ox, encoding);
425                 putMapEntry(map, key, value);
426             }
427         }
428
429     }
430
431
432
433 }