| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.apache.sling.engine.impl.parameters; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.ByteArrayOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.UnsupportedEncodingException; |
| import java.util.LinkedHashMap; |
| import java.util.Map; |
| |
| import org.apache.sling.api.request.RequestParameter; |
| |
| public class Util { |
| |
| // ISO-8859-1 mapps all characters 0..255 to \u0000..\u00ff directly |
| public static final String ENCODING_DIRECT = "ISO-8859-1"; |
| |
| // Default query (and www-form-encoded) parameter encoding as per |
| // HTML spec. |
| // see http://www.w3.org/TR/html4/appendix/notes.html#h-B.2.1 |
| public static final String ENCODING_DEFAULT = "UTF-8"; |
| |
| public static final byte[] NO_CONTENT = new byte[0]; |
| |
| // the default encoding used in #fixEncoding if the _charset_ request |
| // parameter is not set |
| private static String defaultFixEncoding = ENCODING_DIRECT; |
| |
| /** Parse state constant */ |
| private static final int BEFORE_NAME = 0; |
| |
| /** Parse state constant */ |
| private static final int INSIDE_NAME = BEFORE_NAME + 1; |
| |
| /** Parse state constant */ |
| private static final int ESC_NAME = INSIDE_NAME + 1; |
| |
| /** Parse state constant */ |
| private static final int BEFORE_EQU = ESC_NAME + 1; |
| |
| /** Parse state constant */ |
| private static final int BEFORE_VALUE = BEFORE_EQU + 1; |
| |
| /** Parse state constant */ |
| private static final int INSIDE_VALUE = BEFORE_VALUE + 1; |
| |
| /** Parse state constant */ |
| private static final int ESC_VALUE = INSIDE_VALUE + 1; |
| |
| /** Parse state constant */ |
| private static final int AFTER_VALUE = INSIDE_VALUE + 1; |
| |
| /** Parse state constant */ |
| private static final int BEFORE_SEP = AFTER_VALUE + 1; |
| |
| public static void setDefaultFixEncoding(final String encoding) { |
| defaultFixEncoding = validateEncoding(encoding); |
| } |
| |
| static String getDefaultFixEncoding() { |
| return defaultFixEncoding; |
| } |
| |
| static String toIdentityEncodedString(byte[] data) { |
| if (data == null) { |
| return null; |
| } |
| |
| char[] characters = new char[data.length]; |
| for (int i = 0; i < characters.length; i++) { |
| characters[i] = (char) (data[i] & 0xff); |
| } |
| return new String(characters); |
| } |
| |
| static byte[] fromIdentityEncodedString(String string) { |
| if (string == null) { |
| return NO_CONTENT; |
| } |
| |
| byte[] data = new byte[string.length()]; |
| for (int i = 0; i < data.length; i++) { |
| data[i] = (byte) (string.charAt(i) & 0xff); |
| } |
| return data; |
| } |
| |
| static InputStream toInputStream(String source) { |
| byte[] data = fromIdentityEncodedString(source); |
| return new ByteArrayInputStream(data); |
| } |
| |
| static void fixEncoding(ParameterMap parameterMap) { |
| // default the encoding to defaultFixEncoding |
| String formEncoding = getDefaultFixEncoding(); |
| |
| // check whether a form encoding parameter overwrites this default |
| RequestParameter[] feParm = parameterMap.get(ParameterSupport.PARAMETER_FORMENCODING); |
| if (feParm != null) { |
| // get and check form encoding |
| byte[] rawEncoding = feParm[0].get(); |
| formEncoding = toIdentityEncodedString(rawEncoding); |
| formEncoding = validateEncoding(formEncoding); |
| } |
| |
| // map for rename parameters due to encoding fixes |
| LinkedHashMap<String, String> renameMap = new LinkedHashMap<String, String>(); |
| |
| // convert the map of lists to a map of arrays |
| for (Map.Entry<String, RequestParameter[]> paramEntry : parameterMap.entrySet()) { |
| RequestParameter[] params = paramEntry.getValue(); |
| String parName = null; |
| for (int i = 0; i < params.length; i++) { |
| if (params[i] instanceof AbstractRequestParameter) { |
| AbstractRequestParameter param = (AbstractRequestParameter) params[i]; |
| |
| // fix encoding if different |
| if (!formEncoding.equalsIgnoreCase(param.getEncoding())) { |
| param.setEncoding(formEncoding); |
| |
| // prepare the parameter for renaming |
| if (parName == null) { |
| parName = paramEntry.getKey(); |
| String name = reencode(parName, formEncoding); |
| if (!parName.equals(name)) { |
| renameMap.put(parName, name); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| // apply mappings of deinternationalized names |
| if (!renameMap.isEmpty()) { |
| for (Map.Entry<String, String> entry : renameMap.entrySet()) { |
| parameterMap.renameParameter(entry.getKey(), entry.getValue()); |
| } |
| } |
| } |
| |
| private static String reencode(String parName, String encoding) { |
| // re-encode the parameter to the encoding |
| if (!ENCODING_DIRECT.equalsIgnoreCase(encoding)) { |
| try { |
| return new String(parName.getBytes(ENCODING_DIRECT), encoding); |
| } catch (UnsupportedEncodingException uee) { |
| // unexpected, as the encoding is assumed to have been checked ! |
| } |
| } |
| |
| // otherwise just return the name unmodified |
| return parName; |
| } |
| |
| /** |
| * Checks whether the given encoding is known and supported by the platform |
| * or not. If the platform supports the encoding the parameter is returned. |
| * Otherwise or if the encoding argument is <code>null</code> or an empty |
| * string {@link #defaultFixEncoding} is returned. |
| * |
| * @param encoding The encoding to validate |
| * @return The encoding if supported or {@link #defaultFixEncoding} |
| */ |
| private static String validateEncoding(final String encoding) { |
| if (encoding != null && encoding.length() > 0) { |
| // check for the existence of the encoding |
| try { |
| "".getBytes(encoding); |
| return encoding; |
| } catch (UnsupportedEncodingException e) { |
| // log.warn("HttpMulitpartPost: Character encoding {0} is not " |
| // + "supported, using default {1}", formEncodingParam, |
| // DEFAULT_ENCODING); |
| } |
| } |
| |
| // no encoding or unsupported encoding |
| return getDefaultFixEncoding(); |
| } |
| |
| /** |
| * Parse a query string and store entries inside a map |
| * |
| * @param data querystring data |
| * @param encoding encoding to use for converting bytes to characters |
| * @param map map to populate |
| * @param prependNew whether to prepend new values |
| * @throws IllegalArgumentException if the nv string is malformed |
| * @throws UnsupportedEncodingException if the {@code encoding} is not |
| * supported |
| * @throws IOException if an error occurrs reading from {@code data} |
| */ |
| public static void parseQueryString(InputStream data, String encoding, ParameterMap map, boolean prependNew) |
| throws UnsupportedEncodingException, IOException { |
| |
| parseNVPairString(data, encoding, map, '&', false, prependNew); |
| } |
| |
| /** |
| * Parse a name/value pair string and populate a map with key -> value[s] |
| * |
| * @param data name value data |
| * @param encoding encoding to use for converting bytes to characters |
| * @param map map to populate |
| * @param separator multi-value separator character |
| * @param allowSpaces allow spaces inside name/values |
| * @param prependNew whether to prepend new values |
| * @throws IllegalArgumentException if the nv string is malformed |
| * @throws UnsupportedEncodingException if the {@code encoding} is not |
| * supported |
| * @throws IOException if an error occurrs reading from {@code data} |
| */ |
| private static void parseNVPairString( |
| InputStream data, |
| String encoding, |
| ParameterMap map, |
| char separator, |
| boolean allowSpaces, |
| boolean prependNew) |
| throws UnsupportedEncodingException, IOException { |
| |
| ByteArrayOutputStream keyBuffer = new ByteArrayOutputStream(256); |
| ByteArrayOutputStream valueBuffer = new ByteArrayOutputStream(256); |
| char[] chCode = new char[2]; |
| |
| int state = BEFORE_NAME; |
| int subState = 0; |
| |
| for (int in = data.read(); in >= 0; in = data.read()) { |
| char ch = (char) in; |
| |
| switch (state) { |
| case BEFORE_NAME: |
| if (ch == ' ') { |
| continue; |
| } else if (ch == '%') { |
| state = ESC_NAME; |
| subState = 0; |
| } else if (ch == '+' && !allowSpaces) { |
| keyBuffer.write(' '); |
| state = INSIDE_NAME; |
| } else { |
| keyBuffer.write(ch); |
| state = INSIDE_NAME; |
| } |
| break; |
| case INSIDE_NAME: |
| if (ch == '=') { |
| state = BEFORE_VALUE; |
| } else if (ch == '+' && !allowSpaces) { |
| keyBuffer.write(' '); |
| } else if (ch == '%') { |
| state = ESC_NAME; |
| subState = 0; |
| } else if (ch == '&') { |
| addNVPair(map, keyBuffer, valueBuffer, encoding, prependNew); |
| state = BEFORE_NAME; |
| } else { |
| keyBuffer.write(ch); |
| } |
| break; |
| case ESC_NAME: |
| chCode[subState++] = ch; |
| if (subState == chCode.length) { |
| String code = new String(chCode); |
| try { |
| keyBuffer.write(Integer.parseInt(code, 16)); |
| } catch (NumberFormatException e) { |
| throw new IllegalArgumentException("Bad escape sequence: %" + code); |
| } |
| state = INSIDE_NAME; |
| } |
| break; |
| case BEFORE_EQU: |
| if (ch == '=') { |
| state = BEFORE_VALUE; |
| } |
| break; |
| case BEFORE_VALUE: |
| if (ch == ' ') { |
| continue; |
| } else if (ch == '%') { |
| state = ESC_VALUE; |
| subState = 0; |
| } else if (ch == '+' && !allowSpaces) { |
| valueBuffer.write(' '); |
| state = INSIDE_VALUE; |
| } else if (ch == separator) { |
| addNVPair(map, keyBuffer, valueBuffer, encoding, prependNew); |
| state = BEFORE_NAME; |
| } else { |
| valueBuffer.write(ch); |
| state = INSIDE_VALUE; |
| } |
| break; |
| case INSIDE_VALUE: |
| if (ch == separator) { |
| addNVPair(map, keyBuffer, valueBuffer, encoding, prependNew); |
| state = BEFORE_NAME; |
| } else if (ch == '+' && !allowSpaces) { |
| valueBuffer.write(' '); |
| } else if (ch == '%') { |
| state = ESC_VALUE; |
| subState = 0; |
| } else { |
| valueBuffer.write(ch); |
| } |
| break; |
| case ESC_VALUE: |
| chCode[subState++] = ch; |
| if (subState == chCode.length) { |
| String code = new String(chCode); |
| try { |
| valueBuffer.write(Integer.parseInt(code, 16)); |
| } catch (NumberFormatException e) { |
| throw new IllegalArgumentException("Bad escape sequence: %" + code); |
| } |
| state = INSIDE_VALUE; |
| } |
| break; |
| case BEFORE_SEP: |
| if (ch == separator) { |
| state = BEFORE_NAME; |
| } |
| break; |
| } |
| } |
| |
| if (keyBuffer.size() > 0) { |
| addNVPair(map, keyBuffer, valueBuffer, encoding, prependNew); |
| } |
| } |
| |
| private static void addNVPair( |
| ParameterMap map, |
| ByteArrayOutputStream keyBuffer, |
| ByteArrayOutputStream valueBuffer, |
| String encoding, |
| boolean prependNew) |
| throws UnsupportedEncodingException { |
| final String key = keyBuffer.toString(encoding); |
| final String value = valueBuffer.toString(encoding); |
| map.addParameter(new ContainerRequestParameter(key, value, encoding), prependNew); |
| keyBuffer.reset(); |
| valueBuffer.reset(); |
| } |
| } |