blob: 8931faac9acc683617ed44f84244bd52300136dd [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.odata2.core.servlet;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import org.apache.olingo.odata2.api.exception.ODataBadRequestException;
import org.apache.olingo.odata2.api.exception.ODataException;
import org.apache.olingo.odata2.api.exception.ODataNotFoundException;
import org.apache.olingo.odata2.api.exception.ODataUnsupportedMediaTypeException;
import org.apache.olingo.odata2.api.uri.PathInfo;
import org.apache.olingo.odata2.api.uri.PathSegment;
import org.apache.olingo.odata2.core.ODataPathSegmentImpl;
import org.apache.olingo.odata2.core.PathInfoImpl;
import org.apache.olingo.odata2.core.commons.ContentType;
import org.apache.olingo.odata2.core.commons.Decoder;
public class RestUtil {
// RFC 2616, 4.2: linear white space
private static final String REG_EX_OPTIONAL_WHITESPACE = "\\s*";
private static final String REG_EX_FIELD_VALUE_SEPARATOR = "," + REG_EX_OPTIONAL_WHITESPACE;
// RFC 2616, 3.9: qvalue = ("0"["." 0*3DIGIT]) | ("1"["." 0*3("0")])
private static final String REG_EX_QVALUE = "q=((?:1(?:\\.0{0,3})?)|(?:0(?:\\.[0-9]{0,3})?))";
// RFC 2616, 14.1: the media-range parameters
private static final String REG_EX_PARAMETER = "(?:;\\s*(?:(?:[^qQ].*)|(?:[qQ]\\s*=\\s*(?:[^01].*))))*";
private static final Pattern REG_EX_ACCEPT =
Pattern.compile("([a-z\\*\\s]+/[a-zA-Z\\+\\*\\-=\\s]+" + REG_EX_PARAMETER + ")");
private static final Pattern REG_EX_ACCEPT_WITH_Q_FACTOR =
Pattern.compile(REG_EX_ACCEPT + "(?:" + REG_EX_OPTIONAL_WHITESPACE + REG_EX_QVALUE + ")?");
// RFC 2616, 14.4: language-range = ((1*8ALPHA *("-" 1*8ALPHA)) | "*")
private static final Pattern REG_EX_ACCEPT_LANGUAGES =
Pattern.compile("((?:\\*)|(?:[a-z]{1,8}(?:\\-[a-zA-Z]{1,8})?))");
private static final Pattern REG_EX_ACCEPT_LANGUAGES_WITH_Q_FACTOR =
Pattern.compile(REG_EX_ACCEPT_LANGUAGES + "(?:;" + REG_EX_OPTIONAL_WHITESPACE + REG_EX_QVALUE + ")?");
private static final Pattern REG_EX_MATRIX_PARAMETER = Pattern.compile("([^=]*)(?:=(.*))?");
private static final String ACCEPT_FORM_ENCODING = "odata-accept-forms-encoding";
public static ContentType extractRequestContentType(final String contentType)
throws ODataUnsupportedMediaTypeException {
if (contentType == null || contentType.isEmpty()) {
// RFC 2616, 7.2.1:
// "Any HTTP/1.1 message containing an entity-body SHOULD include a
// Content-Type header field defining the media type of that body. [...]
// If the media type remains unknown, the recipient SHOULD treat it
// as type "application/octet-stream"."
return ContentType.APPLICATION_OCTET_STREAM;
} else if (ContentType.isParseable(contentType)) {
return ContentType.create(contentType);
} else {
throw new ODataUnsupportedMediaTypeException(
ODataUnsupportedMediaTypeException.NOT_SUPPORTED_CONTENT_TYPE.addContent(contentType));
}
}
/*
* Parses query parameters.
*/
public static Map<String, String> extractQueryParameters(final String queryString) {
Map<String, String> queryParametersMap = new HashMap<String, String>();
if (queryString != null && queryString.length() > 0) {
// At first the queryString will be decoded.
List<String> queryParameters = Arrays.asList(queryString.split("\\&"));
for (String param : queryParameters) {
String decodedParam = Decoder.decode(param);
int indexOfEqualSign = decodedParam.indexOf("=");
if (indexOfEqualSign < 0) {
queryParametersMap.put(decodedParam, "");
} else {
queryParametersMap.put(decodedParam.substring(0, indexOfEqualSign), decodedParam
.substring(indexOfEqualSign + 1));
}
}
}
return queryParametersMap;
}
public static Map<String, List<String>> extractAllQueryParameters(final String queryString, String formEncoding) {
Map<String, List<String>> allQueryParameterMap = new HashMap<String, List<String>>();
if(Boolean.parseBoolean(formEncoding)){
List<String> encoding = new ArrayList<String>();
encoding.add(formEncoding);
allQueryParameterMap.put(ACCEPT_FORM_ENCODING, encoding );
}
if (queryString != null && queryString.length() > 0) {
// At first the queryString will be decoded.
List<String> queryParameters = Arrays.asList(queryString.split("\\&"));
for (String param : queryParameters) {
String decodedParam = Decoder.decode(param);
int indexOfEqualSign = decodedParam.indexOf("=");
if (indexOfEqualSign < 0) {
final List<String> parameterList =
allQueryParameterMap.containsKey(decodedParam) ? allQueryParameterMap.get(decodedParam)
: new LinkedList<String>();
allQueryParameterMap.put(decodedParam, parameterList);
parameterList.add("");
} else {
final String key = decodedParam.substring(0, indexOfEqualSign);
final List<String> parameterList = allQueryParameterMap.containsKey(key) ? allQueryParameterMap.get(key)
: new LinkedList<String>();
allQueryParameterMap.put(key, parameterList);
parameterList.add(decodedParam.substring(indexOfEqualSign + 1));
}
}
}
return allQueryParameterMap;
}
/*
* Parses Accept-Language header. Returns a list sorted by quality parameter
*/
public static List<Locale> extractAcceptableLanguage(final String acceptableLanguageHeader) {
List<Locale> acceptLanguages = new ArrayList<Locale>();
TreeSet<Accept> acceptTree = getAcceptTree();
if (acceptableLanguageHeader != null && !acceptableLanguageHeader.isEmpty()) {
List<String> list = Arrays.asList(acceptableLanguageHeader.split(REG_EX_FIELD_VALUE_SEPARATOR));
for (String acceptLanguage : list) {
Matcher matcher = REG_EX_ACCEPT_LANGUAGES_WITH_Q_FACTOR.matcher(acceptLanguage);
if (matcher.find()) {
String language = matcher.group(1);
double qualityFactor = matcher.group(2) != null ? Double.parseDouble(matcher.group(2)) : 1d;
acceptTree.add(new Accept(language, qualityFactor));
}
}
}
for (Accept accept : acceptTree) {
String languageRange = accept.getValue();
// The languageRange has to be splitted in language tag and country tag
int indexOfMinus = languageRange.indexOf("-");
Locale locale;
if (indexOfMinus < 0) {
// no country tag
locale = new Locale(languageRange);
} else {
String language = languageRange.substring(0, indexOfMinus);
String country = languageRange.substring(indexOfMinus + 1);
locale = new Locale(language, country);
}
acceptLanguages.add(locale);
}
return acceptLanguages;
}
/*
* Parses Accept header. Returns a list of media ranges sorted by quality parameter
*/
public static List<String> extractAcceptHeaders(final String acceptHeader) {
TreeSet<Accept> acceptTree = getAcceptTree();
List<String> acceptHeaders = new ArrayList<String>();
if (acceptHeader != null && !acceptHeader.isEmpty()) {
List<String> list = Arrays.asList(acceptHeader.split(REG_EX_FIELD_VALUE_SEPARATOR));
for (String accept : list) {
Matcher matcher = REG_EX_ACCEPT_WITH_Q_FACTOR.matcher(accept);
if (matcher.find()) {
String headerValue = matcher.group(1);
double qualityFactor = matcher.group(2) != null ? Double.parseDouble(matcher.group(2)) : 1d;
acceptTree.add(new Accept(headerValue, qualityFactor));
}
}
}
for (Accept accept : acceptTree) {
acceptHeaders.add(accept.getValue());
}
return acceptHeaders;
}
@SuppressWarnings("unchecked")
public static Map<String, List<String>> extractHeaders(final HttpServletRequest req) {
Map<String, List<String>> requestHeaders = new HashMap<String, List<String>>();
for (Enumeration<String> headerNames = req.getHeaderNames(); headerNames.hasMoreElements();) {
String headerName = headerNames.nextElement();
List<String> headerValues = new ArrayList<String>();
for (Enumeration<String> headers = req.getHeaders(headerName); headers.hasMoreElements();) {
String value = headers.nextElement();
headerValues.add(value);
}
if (requestHeaders.containsKey(headerName)) {
requestHeaders.get(headerName).addAll(headerValues);
} else {
requestHeaders.put(headerName, headerValues);
}
}
return requestHeaders;
}
public static PathInfo buildODataPathInfo(final HttpServletRequest req, final int pathSplit) throws ODataException {
PathInfoImpl pathInfo = splitPath(req, pathSplit);
pathInfo.setServiceRoot(buildBaseUri(req, pathInfo.getPrecedingSegments()));
pathInfo.setRequestUri(buildRequestUri(req));
return pathInfo;
}
private static URI buildBaseUri(final HttpServletRequest req, final List<PathSegment> precedingPathSegments)
throws ODataException {
try {
URI baseUri;
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(req.getContextPath()).append(req.getServletPath());
for (final PathSegment ps : precedingPathSegments) {
if (!"".equals(ps.getPath()) && ps.getPath().length() > 0) {
stringBuilder.append("/").append(ps.getPath());
}
Map<String, List<String>> matrixParams = ps.getMatrixParameters();
for (final String key : matrixParams.keySet()) {
List<String> matrixParameters = matrixParams.get(key);
String matrixParameterString = ";" + key + "=";
for (String matrixParam : matrixParameters) {
matrixParameterString += (matrixParam.length() > 0) ? (Decoder.decode(matrixParam) + ",") : "";
}
stringBuilder.append(matrixParameterString.substring(0, matrixParameterString.length() - 1));
}
}
String path = stringBuilder.toString();
if (!path.endsWith("/")) {
path = path + "/";
}
baseUri = new URI(req.getScheme(), null, req.getServerName(), req.getServerPort(), path, null, null);
return baseUri;
} catch (final URISyntaxException e) {
throw new ODataException(e);
}
}
private static URI buildRequestUri(final HttpServletRequest servletRequest) {
URI requestUri;
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(servletRequest.getRequestURL());
String queryString = servletRequest.getQueryString();
if (queryString != null) {
stringBuilder.append("?").append(queryString);
}
String requestUriString = stringBuilder.toString();
requestUri = URI.create(requestUriString);
return requestUri;
}
private static PathInfoImpl splitPath(final HttpServletRequest servletRequest, final int pathSplit)
throws ODataException {
PathInfoImpl pathInfo = new PathInfoImpl();
List<String> precedingPathSegments;
List<String> pathSegments;
String pathInfoString = extractPathInfo(servletRequest);
while (pathInfoString.startsWith("/")) {
pathInfoString = pathInfoString.substring(1);
}
List<String> segments = null;
// EmptyStrings have to result in an empty list.
// Since split will always deliver an empty string back we have to do this manually
if (pathInfoString.isEmpty()) {
segments = new ArrayList<String>();
} else {
segments = Arrays.asList(pathInfoString.split("/", -1));
}
if (pathSplit == 0) {
precedingPathSegments = Collections.emptyList();
pathSegments = segments;
} else {
if (segments.size() < pathSplit) {
throw new ODataBadRequestException(ODataBadRequestException.URLTOOSHORT);
}
precedingPathSegments = segments.subList(0, pathSplit);
final int pathSegmentCount = segments.size();
pathSegments = segments.subList(pathSplit, pathSegmentCount);
}
// Percent-decode only the preceding path segments.
// The OData path segments are decoded during URI parsing.
pathInfo.setPrecedingPathSegment(convertPathSegmentList(precedingPathSegments));
List<PathSegment> odataSegments = new ArrayList<PathSegment>();
for (final String segment : pathSegments) {
int index = segment.indexOf(";");
if (index < 0) {
odataSegments.add(new ODataPathSegmentImpl(segment, null));
} else {
// post condition: we do not allow matrix parameters in OData path segments
String path = segment.substring(0, index);
Map<String, List<String>> parameterMap = extractMatrixParameter(segment, index);
throw new ODataNotFoundException(ODataNotFoundException.MATRIX.addContent(parameterMap.keySet(), path));
}
}
pathInfo.setODataPathSegment(odataSegments);
return pathInfo;
}
private static List<PathSegment> convertPathSegmentList(final List<String> pathSegments) {
ArrayList<PathSegment> converted = new ArrayList<PathSegment>();
for (final String segment : pathSegments) {
int index = segment.indexOf(";");
if (index == -1) {
converted.add(new ODataPathSegmentImpl(Decoder.decode(segment), null));
} else {
String path = segment.substring(0, index);
Map<String, List<String>> parameterMap = extractMatrixParameter(segment, index);
converted.add(new ODataPathSegmentImpl(Decoder.decode(path), parameterMap));
}
}
return converted;
}
private static Map<String, List<String>> extractMatrixParameter(final String segment, final int index) {
List<String> matrixParameters = Arrays.asList(segment.substring(index + 1).split(";"));
String matrixParameterName = "";
String matrixParamaterValues = "";
Map<String, List<String>> parameterMap = new HashMap<String, List<String>>();
for (String matrixParameter : matrixParameters) {
List<String> values = Arrays.asList("");
Matcher matcher = REG_EX_MATRIX_PARAMETER.matcher(matrixParameter);
if (matcher.find()) {
matrixParameterName = matcher.group(1);
matrixParamaterValues = matcher.group(2);
}
if (matrixParamaterValues != null) {
values = Arrays.asList(matrixParamaterValues.split(","));
}
parameterMap.put(matrixParameterName, values);
}
return parameterMap;
}
private static String extractPathInfo(final HttpServletRequest servletRequest) {
String pathInfoString;
final String requestUri = servletRequest.getRequestURI();
pathInfoString = requestUri;
int index = requestUri.indexOf(servletRequest.getContextPath());
if (index >= 0) {
pathInfoString = pathInfoString.substring(servletRequest.getContextPath().length());
}
int indexServletPath = pathInfoString.indexOf(servletRequest.getServletPath());
if (indexServletPath >= 0) {
int substringFromPos = indexServletPath + servletRequest.getServletPath().length();
pathInfoString = pathInfoString.substring(substringFromPos);
}
return pathInfoString;
}
private static TreeSet<Accept> getAcceptTree() {
TreeSet<Accept> treeSet = new TreeSet<Accept>(new Comparator<Accept>() {
@Override
public int compare(final Accept header1, final Accept header2) {
if (header1.getQuality() <= header2.getQuality()) {
return 1;
} else {
return -1;
}
}
});
return treeSet;
}
/*
* The class is used in order to sort headers by "q" parameter.
* The object of this class contains a value of the Accept header or Accept-Language header and value of the
* quality parameter.
*/
private static class Accept {
private double quality;
private String value;
public Accept(final String headerValue, final double qualityFactor) {
value = headerValue;
quality = qualityFactor;
}
public String getValue() {
return value;
}
public double getQuality() {
return quality;
}
}
}