blob: bff2a9c015b9f197a95b4135387d257e5a92cbf4 [file] [log] [blame]
/*
* 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.olingo.server.core.prefer;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.olingo.server.api.prefer.Preferences.Preference;
/**
* <p>Parses the values of <code>Prefer</code> HTTP header fields.</p>
* <p>See <a href="https://www.ietf.org/rfc/rfc7240.txt">RFC 7240</a> for details;
* there the following grammar is defined:</p>
* <pre>
* Prefer = "Prefer" ":" 1#preference
* preference = token [ BWS "=" BWS word ] *( OWS ";" [ OWS parameter ] )
* parameter = token [ BWS "=" BWS word ]
* token = 1*tchar
* tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
* / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA
* word = token / quoted-string
* quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE
* qdtext = HTAB / SP / %x21 / %x23-5B / %x5D-7E / %x80-FF
* quoted-pair = "\" ( HTAB / SP / %x21-7E / %x80-FF )
* OWS = *( SP / HTAB ) ; optional whitespace
* BWS = OWS ; "bad" whitespace
* </pre>
* <p>Values with illegal syntax do not contribute to the result but no exception is thrown.</p>
*/
public class PreferParser {
private static final String TOKEN = "(?:[-!#$%&'*+.^_`|~]|\\w)+";
private static final String QUOTED_STRING = "(?:\"(?:[\\t !#-\\[\\]-~\\x80-\\xFF]|"
+ "(?:\\\\[\\t !-~\\x80-\\xFF]))*\")";
private static final String NAMED_VALUE =
"(" + TOKEN + ")(?:\\s*=\\s*(" + TOKEN + "|" + QUOTED_STRING + "))?";
private static final Pattern PREFERENCE = Pattern.compile("\\s*(,\\s*)+|"
+ "(?:" + NAMED_VALUE + "((?:\\s*;\\s*(?:" + NAMED_VALUE + ")?)*))");
private static final Pattern PARAMETER = Pattern.compile("\\s*(;\\s*)+|(?:" + NAMED_VALUE + ")");
private PreferParser() {
// Private constructor for utility classes
}
protected static Map<String, Preference> parse(final Collection<String> values) {
if (values == null || values.isEmpty()) {
return Collections.emptyMap();
}
Map<String, Preference> result = new HashMap<>();
for (final String value : values) {
if (value != null && !value.isEmpty()) {
parse(value, result);
}
}
return result;
}
private static void parse(final String value, final Map<String, Preference> result) {
Map<String, Preference> partResult = new HashMap<>();
String separator = "";
int start = 0;
Matcher matcher = PREFERENCE.matcher(value.trim());
while (matcher.find() && matcher.start() == start) {
start = matcher.end();
if (matcher.group(1) != null) {
separator = matcher.group(1);
} else if (separator != null) {
final String name = matcher.group(2).toLowerCase(Locale.ROOT);
// RFC 7240:
// If any preference is specified more than once, only the first instance is to be
// considered. All subsequent occurrences SHOULD be ignored without signaling
// an error or otherwise altering the processing of the request.
if (!partResult.containsKey(name)) {
final String preferenceValue = getValue(matcher.group(3));
final Map<String, String> parameters =
matcher.group(4) == null || matcher.group(4).isEmpty() ? null :
parseParameters(matcher.group(4));
partResult.put(name, new Preference(preferenceValue, parameters));
}
separator = null;
} else {
return;
}
}
if (matcher.hitEnd()) {
// Here we also have to keep already existing preferences.
for (final Map.Entry<String, Preference> entry : partResult.entrySet()) {
if (!result.containsKey(entry.getKey())) {
result.put(entry.getKey(), entry.getValue());
}
}
}
}
private static Map<String, String> parseParameters(final String parameters) {
Map<String, String> result = new HashMap<>();
String separator = "";
int start = 0;
Matcher matcher = PARAMETER.matcher(parameters.trim());
while (matcher.find() && matcher.start() == start) {
start = matcher.end();
if (matcher.group(1) != null) {
separator = matcher.group(1);
} else if (separator != null) {
final String name = matcher.group(2).toLowerCase(Locale.ROOT);
// We have to keep already existing parameters.
if (!result.containsKey(name)) {
result.put(name, getValue(matcher.group(3)));
}
separator = null;
} else {
return null;
}
}
return matcher.hitEnd() ? Collections.unmodifiableMap(result) : null;
}
private static String getValue(final String value) {
if (value == null) {
return null;
}
String result = value;
if (value.startsWith("\"")) {
result = value.substring(1, value.length() - 1);
}
// Unquote backslash-quoted characters.
if (result.indexOf('\\') >= 0) {
result = result.replaceAll("\\\\(.)", "$1");
}
return result;
}
}