/******************************************************************************* * Copyright 2013 SAP AG * * Licensed 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 com.sap.core.odata.core; import java.util.Arrays; import java.util.List; import java.util.Map; import com.sap.core.odata.api.ODataService; import com.sap.core.odata.api.ODataServiceFactory; import com.sap.core.odata.api.ODataServiceVersion; import com.sap.core.odata.api.commons.HttpStatusCodes; import com.sap.core.odata.api.commons.ODataHttpHeaders; import com.sap.core.odata.api.commons.ODataHttpMethod; import com.sap.core.odata.api.edm.EdmException; import com.sap.core.odata.api.edm.EdmProperty; import com.sap.core.odata.api.edm.EdmSimpleTypeKind; import com.sap.core.odata.api.exception.ODataBadRequestException; import com.sap.core.odata.api.exception.ODataException; import com.sap.core.odata.api.exception.ODataMethodNotAllowedException; import com.sap.core.odata.api.exception.ODataUnsupportedMediaTypeException; import com.sap.core.odata.api.processor.ODataContext; import com.sap.core.odata.api.processor.ODataProcessor; import com.sap.core.odata.api.processor.ODataRequest; import com.sap.core.odata.api.processor.ODataResponse; import com.sap.core.odata.api.processor.ODataResponse.ODataResponseBuilder; import com.sap.core.odata.api.processor.part.EntityLinkProcessor; import com.sap.core.odata.api.processor.part.EntityLinksProcessor; import com.sap.core.odata.api.processor.part.EntityMediaProcessor; import com.sap.core.odata.api.processor.part.EntityProcessor; import com.sap.core.odata.api.processor.part.EntitySetProcessor; import com.sap.core.odata.api.processor.part.EntitySimplePropertyValueProcessor; import com.sap.core.odata.api.processor.part.FunctionImportProcessor; import com.sap.core.odata.api.processor.part.FunctionImportValueProcessor; import com.sap.core.odata.api.uri.PathSegment; import com.sap.core.odata.api.uri.UriInfo; import com.sap.core.odata.api.uri.UriParser; import com.sap.core.odata.core.commons.ContentType; import com.sap.core.odata.core.commons.ContentType.ODataFormat; import com.sap.core.odata.core.debug.ODataDebugResponseWrapper; import com.sap.core.odata.core.exception.ODataRuntimeException; import com.sap.core.odata.core.uri.UriInfoImpl; import com.sap.core.odata.core.uri.UriParserImpl; import com.sap.core.odata.core.uri.UriType; /** * @author SAP AG */ public class ODataRequestHandler { private final ODataServiceFactory serviceFactory; private final ODataService service; private final ODataContext context; public ODataRequestHandler(final ODataServiceFactory factory, final ODataService service, final ODataContext context) { serviceFactory = factory; this.service = service; this.context = context; } /** * <p>Handles the {@link ODataRequest} in a way that it results in a corresponding {@link ODataResponse}.</p> * <p>This includes delegation of URI parsing and dispatching of the request internally. * Building of the {@link ODataContext} takes place outside of this method.</p> * @param request the incoming request * @return the corresponding result */ public ODataResponse handle(final ODataRequest request) { UriInfoImpl uriInfo = null; Exception exception = null; ODataResponse odataResponse; final int timingHandle = context.startRuntimeMeasurement("ODataRequestHandler", "handle"); try { UriParser uriParser = new UriParserImpl(service.getEntityDataModel()); Dispatcher dispatcher = new Dispatcher(serviceFactory, service); final String serverDataServiceVersion = getServerDataServiceVersion(); final String requestDataServiceVersion = context.getRequestHeader(ODataHttpHeaders.DATASERVICEVERSION); validateDataServiceVersion(serverDataServiceVersion, requestDataServiceVersion); final List<PathSegment> pathSegments = context.getPathInfo().getODataSegments(); int timingHandle2 = context.startRuntimeMeasurement("UriParserImpl", "parse"); uriInfo = (UriInfoImpl) uriParser.parse(pathSegments, request.getQueryParameters()); context.stopRuntimeMeasurement(timingHandle2); final ODataHttpMethod method = request.getMethod(); final UriType uriType = uriInfo.getUriType(); validateMethodAndUri(method, uriInfo); if (method == ODataHttpMethod.POST || method == ODataHttpMethod.PUT || method == ODataHttpMethod.PATCH || method == ODataHttpMethod.MERGE) { checkRequestContentType(uriInfo, request.getContentType()); } final String acceptContentType = new ContentNegotiator().doContentNegotiation(uriInfo, request.getAcceptHeaders(), getSupportedContentTypes(uriInfo)); timingHandle2 = context.startRuntimeMeasurement("Dispatcher", "dispatch"); odataResponse = dispatcher.dispatch(method, uriInfo, request.getBody(), request.getContentType(), acceptContentType); context.stopRuntimeMeasurement(timingHandle2); final String location = (method == ODataHttpMethod.POST && (uriType == UriType.URI1 || uriType == UriType.URI6B)) ? odataResponse.getIdLiteral() : null; final HttpStatusCodes s = odataResponse.getStatus() == null ? method == ODataHttpMethod.POST ? uriType == UriType.URI9 ? HttpStatusCodes.OK : uriType == UriType.URI7B ? HttpStatusCodes.NO_CONTENT : HttpStatusCodes.CREATED : method == ODataHttpMethod.PUT || method == ODataHttpMethod.PATCH || method == ODataHttpMethod.MERGE || method == ODataHttpMethod.DELETE ? HttpStatusCodes.NO_CONTENT : HttpStatusCodes.OK : odataResponse.getStatus(); ODataResponseBuilder extendedResponse = ODataResponse.fromResponse(odataResponse); if (!odataResponse.containsHeader(ODataHttpHeaders.DATASERVICEVERSION)) { extendedResponse = extendedResponse.header(ODataHttpHeaders.DATASERVICEVERSION, serverDataServiceVersion); } extendedResponse = extendedResponse.idLiteral(location).status(s); odataResponse = extendedResponse.build(); } catch (final Exception e) { exception = e; odataResponse = new ODataExceptionWrapper(context, request.getQueryParameters(), request.getAcceptHeaders()).wrapInExceptionResponse(e); } context.stopRuntimeMeasurement(timingHandle); final String debugValue = getDebugValue(context, request.getQueryParameters()); return debugValue == null ? odataResponse : new ODataDebugResponseWrapper(context, odataResponse, uriInfo, exception, debugValue).wrapResponse(); } private String getServerDataServiceVersion() throws ODataException { return service.getVersion() == null ? ODataServiceVersion.V20 : service.getVersion(); } private static void validateDataServiceVersion(final String serverDataServiceVersion, final String requestDataServiceVersion) throws ODataException { if (requestDataServiceVersion != null) { try { final boolean isValid = ODataServiceVersion.validateDataServiceVersion(requestDataServiceVersion); if (!isValid || ODataServiceVersion.isBiggerThan(requestDataServiceVersion, serverDataServiceVersion)) { throw new ODataBadRequestException(ODataBadRequestException.VERSIONERROR.addContent(requestDataServiceVersion.toString())); } } catch (final IllegalArgumentException e) { throw new ODataBadRequestException(ODataBadRequestException.PARSEVERSIONERROR.addContent(requestDataServiceVersion), e); } } } private static void validateMethodAndUri(final ODataHttpMethod method, final UriInfoImpl uriInfo) throws ODataException { validateUriMethod(method, uriInfo); checkFunctionImport(method, uriInfo); if (method != ODataHttpMethod.GET) { checkNotGetSystemQueryOptions(method, uriInfo); checkNumberOfNavigationSegments(uriInfo); checkProperty(method, uriInfo); } } private static void validateUriMethod(final ODataHttpMethod method, final UriInfoImpl uriInfo) throws ODataException { switch (uriInfo.getUriType()) { case URI0: case URI8: case URI15: case URI16: case URI50A: case URI50B: if (method != ODataHttpMethod.GET) { throw new ODataMethodNotAllowedException(ODataMethodNotAllowedException.DISPATCH); } break; case URI1: case URI6B: case URI7B: if (method != ODataHttpMethod.GET && method != ODataHttpMethod.POST) { throw new ODataMethodNotAllowedException(ODataMethodNotAllowedException.DISPATCH); } break; case URI2: case URI6A: case URI7A: if (method != ODataHttpMethod.GET && method != ODataHttpMethod.PUT && method != ODataHttpMethod.DELETE && method != ODataHttpMethod.PATCH && method != ODataHttpMethod.MERGE) { throw new ODataMethodNotAllowedException(ODataMethodNotAllowedException.DISPATCH); } break; case URI3: if (method != ODataHttpMethod.GET && method != ODataHttpMethod.PUT && method != ODataHttpMethod.PATCH && method != ODataHttpMethod.MERGE) { throw new ODataMethodNotAllowedException(ODataMethodNotAllowedException.DISPATCH); } break; case URI4: case URI5: if (method != ODataHttpMethod.GET && method != ODataHttpMethod.PUT && method != ODataHttpMethod.DELETE && method != ODataHttpMethod.PATCH && method != ODataHttpMethod.MERGE) { throw new ODataMethodNotAllowedException(ODataMethodNotAllowedException.DISPATCH); } else if (method == ODataHttpMethod.DELETE && !uriInfo.isValue()) { throw new ODataMethodNotAllowedException(ODataMethodNotAllowedException.DISPATCH); } break; case URI9: if (method != ODataHttpMethod.POST) { throw new ODataMethodNotAllowedException(ODataMethodNotAllowedException.DISPATCH); } break; case URI10: case URI11: case URI12: case URI13: case URI14: break; case URI17: if (method != ODataHttpMethod.GET && method != ODataHttpMethod.PUT && method != ODataHttpMethod.DELETE) { throw new ODataMethodNotAllowedException(ODataMethodNotAllowedException.DISPATCH); } break; default: throw new ODataRuntimeException("Unknown or not implemented URI type: " + uriInfo.getUriType()); } } private static void checkFunctionImport(final ODataHttpMethod method, final UriInfoImpl uriInfo) throws ODataException { if (uriInfo.getFunctionImport() != null && uriInfo.getFunctionImport().getHttpMethod() != null && !uriInfo.getFunctionImport().getHttpMethod().equals(method.toString())) { throw new ODataMethodNotAllowedException(ODataMethodNotAllowedException.DISPATCH); } } private static void checkNotGetSystemQueryOptions(final ODataHttpMethod method, final UriInfoImpl uriInfo) throws ODataException { switch (uriInfo.getUriType()) { case URI1: case URI6B: if (uriInfo.getFormat() != null || uriInfo.getFilter() != null || uriInfo.getInlineCount() != null || uriInfo.getOrderBy() != null || uriInfo.getSkipToken() != null || uriInfo.getSkip() != null || uriInfo.getTop() != null || !uriInfo.getExpand().isEmpty() || !uriInfo.getSelect().isEmpty()) { throw new ODataMethodNotAllowedException(ODataMethodNotAllowedException.DISPATCH); } break; case URI2: if (uriInfo.getFormat() != null || !uriInfo.getExpand().isEmpty() || !uriInfo.getSelect().isEmpty()) { throw new ODataMethodNotAllowedException(ODataMethodNotAllowedException.DISPATCH); } if (method == ODataHttpMethod.DELETE) { if (uriInfo.getFilter() != null) { throw new ODataMethodNotAllowedException(ODataMethodNotAllowedException.DISPATCH); } } break; case URI3: if (uriInfo.getFormat() != null) { throw new ODataMethodNotAllowedException(ODataMethodNotAllowedException.DISPATCH); } break; case URI4: case URI5: if (method == ODataHttpMethod.PUT || method == ODataHttpMethod.PATCH || method == ODataHttpMethod.MERGE) { if (!uriInfo.isValue() && uriInfo.getFormat() != null) { throw new ODataMethodNotAllowedException(ODataMethodNotAllowedException.DISPATCH); } } break; case URI7A: if (uriInfo.getFormat() != null || uriInfo.getFilter() != null) { throw new ODataMethodNotAllowedException(ODataMethodNotAllowedException.DISPATCH); } break; case URI7B: if (uriInfo.getFormat() != null || uriInfo.getFilter() != null || uriInfo.getInlineCount() != null || uriInfo.getOrderBy() != null || uriInfo.getSkipToken() != null || uriInfo.getSkip() != null || uriInfo.getTop() != null) { throw new ODataMethodNotAllowedException(ODataMethodNotAllowedException.DISPATCH); } break; case URI17: if (uriInfo.getFormat() != null || uriInfo.getFilter() != null) { throw new ODataMethodNotAllowedException(ODataMethodNotAllowedException.DISPATCH); } break; default: break; } } private static void checkNumberOfNavigationSegments(final UriInfoImpl uriInfo) throws ODataException { switch (uriInfo.getUriType()) { case URI1: case URI6B: case URI7A: case URI7B: if (uriInfo.getNavigationSegments().size() > 1) { throw new ODataBadRequestException(ODataBadRequestException.NOTSUPPORTED); } break; case URI3: case URI4: case URI5: case URI17: if (!uriInfo.getNavigationSegments().isEmpty()) { throw new ODataBadRequestException(ODataBadRequestException.NOTSUPPORTED); } break; default: break; } } private static void checkProperty(final ODataHttpMethod method, final UriInfoImpl uriInfo) throws ODataException { if ((uriInfo.getUriType() == UriType.URI4 || uriInfo.getUriType() == UriType.URI5) && (isPropertyKey(uriInfo) || method == ODataHttpMethod.DELETE && !isPropertyNullable(getProperty(uriInfo)))) { throw new ODataMethodNotAllowedException(ODataMethodNotAllowedException.DISPATCH); } } private static EdmProperty getProperty(final UriInfo uriInfo) { final List<EdmProperty> propertyPath = uriInfo.getPropertyPath(); return propertyPath == null || propertyPath.isEmpty() ? null : propertyPath.get(propertyPath.size() - 1); } private static boolean isPropertyKey(final UriInfo uriInfo) throws EdmException { return uriInfo.getTargetEntitySet().getEntityType().getKeyProperties().contains(getProperty(uriInfo)); } private static boolean isPropertyNullable(final EdmProperty property) throws EdmException { return property.getFacets() == null || property.getFacets().isNullable(); } /** * <p>Checks if <code>content type</code> is a valid request content type for the given {@link UriInfoImpl}.</p> * <p>If the combination of <code>content type</code> and {@link UriInfoImpl} * is not valid, an {@link ODataUnsupportedMediaTypeException} is thrown.</p> * @param uriInfo information about request URI * @param contentType request content type * @throws ODataException in the case of an error during {@link UriInfoImpl} access; * if the combination of <code>content type</code> and {@link UriInfoImpl} * is invalid, as {@link ODataUnsupportedMediaTypeException} */ private void checkRequestContentType(final UriInfoImpl uriInfo, final String contentType) throws ODataException { Class<? extends ODataProcessor> processorFeature = Dispatcher.mapUriTypeToProcessorFeature(uriInfo); // Don't check the request content type for function imports // because the request body is not used at all. if (processorFeature == FunctionImportProcessor.class || processorFeature == FunctionImportValueProcessor.class) { return; } // Adjust processor feature. if (processorFeature == EntitySetProcessor.class) { processorFeature = uriInfo.getTargetEntitySet().getEntityType().hasStream() ? EntityMediaProcessor.class : // A media resource can have any type. EntityProcessor.class; // The request must contain a single entity! } else if (processorFeature == EntityLinksProcessor.class) { processorFeature = EntityLinkProcessor.class; // The request must contain a single link! } final ContentType parsedContentType = ContentType.parse(contentType); if (parsedContentType == null || parsedContentType.hasWildcard()) { throw new ODataUnsupportedMediaTypeException(ODataUnsupportedMediaTypeException.NOT_SUPPORTED.addContent(parsedContentType)); } // Get list of supported content types based on processor feature. final List<ContentType> supportedContentTypes = processorFeature == EntitySimplePropertyValueProcessor.class ? getSupportedContentTypes(getProperty(uriInfo)) : getSupportedContentTypes(processorFeature); if (!hasMatchingContentType(parsedContentType, supportedContentTypes)) { throw new ODataUnsupportedMediaTypeException(ODataUnsupportedMediaTypeException.NOT_SUPPORTED.addContent(parsedContentType)); } } /** * Checks if the given list of {@link ContentType}s contains a matching {@link ContentType} * for the given <code>contentType</code> parameter. * @param contentType for which a matching content type is searched * @param allowedContentTypes list against which is checked for possible matching {@link ContentType}s * @return <code>true</code> if a matching content type is in given list, otherwise <code>false</code> */ private static boolean hasMatchingContentType(final ContentType contentType, final List<ContentType> allowedContentTypes) { final ContentType requested = contentType.receiveWithCharsetParameter(ContentNegotiator.DEFAULT_CHARSET); if (requested.getODataFormat() == ODataFormat.CUSTOM || requested.getODataFormat() == ODataFormat.MIME) { return requested.hasCompatible(allowedContentTypes); } return requested.hasMatch(allowedContentTypes); } private static List<ContentType> getSupportedContentTypes(final EdmProperty property) throws EdmException { return property.getType() == EdmSimpleTypeKind.Binary.getEdmSimpleTypeInstance() ? Arrays.asList(property.getMimeType() == null ? ContentType.WILDCARD : ContentType.create(property.getMimeType())) : Arrays.asList(ContentType.TEXT_PLAIN, ContentType.TEXT_PLAIN_CS_UTF_8); } private List<String> getSupportedContentTypes(final UriInfoImpl uriInfo) throws ODataException { return service.getSupportedContentTypes(Dispatcher.mapUriTypeToProcessorFeature(uriInfo)); } private List<ContentType> getSupportedContentTypes(final Class<? extends ODataProcessor> processorFeature) throws ODataException { return ContentType.create(service.getSupportedContentTypes(processorFeature)); } private static String getDebugValue(final ODataContext context, final Map<String, String> queryParameters) { return context.isInDebugMode() ? getQueryDebugValue(queryParameters) : null; } private static String getQueryDebugValue(final Map<String, String> queryParameters) { final String debugValue = queryParameters.get(ODataDebugResponseWrapper.ODATA_DEBUG_QUERY_PARAMETER); return ODataDebugResponseWrapper.ODATA_DEBUG_JSON.equals(debugValue) ? debugValue : null; } }