| /******************************************************************************* |
| * 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; |
| } |
| |
| } |
| |
| } |