com.sap.cds.adapter.odata.v4.CdsODataV4Servlet Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of cds-adapter-odata-v4 Show documentation
Show all versions of cds-adapter-odata-v4 Show documentation
OData V4 adapter for CDS Services Java
/**************************************************************************
* (C) 2019-2024 SAP SE or an SAP affiliate company. All rights reserved. *
**************************************************************************/
package com.sap.cds.adapter.odata.v4;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Stream;
import org.apache.commons.lang3.tuple.MutablePair;
import org.apache.olingo.commons.api.edm.provider.CsdlEdmProvider;
import org.apache.olingo.commons.api.format.ContentType;
import org.apache.olingo.commons.api.http.HttpHeader;
import org.apache.olingo.server.api.OData;
import org.apache.olingo.server.api.ODataHttpHandler;
import org.apache.olingo.server.api.ServiceMetadata;
import org.apache.olingo.server.api.etag.ServiceMetadataETagSupport;
import org.apache.olingo.server.core.uri.parser.search.SearchParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.annotations.VisibleForTesting;
import com.sap.cds.adapter.edmx.EdmxI18nProvider;
import com.sap.cds.adapter.edmx.EdmxV4Provider;
import com.sap.cds.adapter.odata.v4.metadata.CustomMetadataProcessor;
import com.sap.cds.adapter.odata.v4.metadata.CustomServiceDocumentProcessor;
import com.sap.cds.adapter.odata.v4.metadata.ODataExtendedEdmProvider;
import com.sap.cds.adapter.odata.v4.metadata.SimpleETagSupport;
import com.sap.cds.adapter.odata.v4.metadata.cds.CdsServiceEdmProvider;
import com.sap.cds.adapter.odata.v4.metadata.provider.LocalizingEdmxProviderWrapper;
import com.sap.cds.adapter.odata.v4.processors.OlingoProcessor;
import com.sap.cds.adapter.odata.v4.utils.ODataUtils;
import com.sap.cds.adapter.odata.v4.utils.QueryLimitUtils;
import com.sap.cds.services.ErrorStatuses;
import com.sap.cds.services.ServiceException;
import com.sap.cds.services.cds.ApplicationService;
import com.sap.cds.services.changeset.ChangeSetContext;
import com.sap.cds.services.changeset.ChangeSetContextSPI;
import com.sap.cds.services.request.ParameterInfo;
import com.sap.cds.services.runtime.CdsRuntime;
import com.sap.cds.services.utils.CdsErrorStatuses;
import com.sap.cds.services.utils.ErrorStatusException;
import com.sap.cds.services.utils.StringUtils;
import com.sap.cds.services.utils.path.CdsServicePath;
import com.sap.cds.services.utils.path.UrlResourcePathBuilder;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@SuppressWarnings("serial")
public class CdsODataV4Servlet extends HttpServlet {
private static final Logger logger = LoggerFactory.getLogger(CdsODataV4Servlet.class);
private static final String UNEXPECTED_ERROR_MESSAGE = "An unexpected error occurred during servlet processing";
private final Map servicePaths; //NOSONAR
private final CdsRuntime runtime; //NOSONAR
private final String adapterBasePath;
public CdsODataV4Servlet(CdsRuntime runtime, String adapterBasePath) {
this.runtime = runtime;
this.adapterBasePath = adapterBasePath;
this.servicePaths = CdsServicePath.basePaths(runtime, CdsODataV4ServletFactory.PROTOCOL_KEY);
// initialize caches
QueryLimitUtils.initialize(runtime);
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// extract the locale according to request parameters
ParameterInfo parameterInfo = runtime.getProvidedParameterInfo();
Locale locale = parameterInfo.getLocale();
AtomicBoolean unclosedChangeSetTracker = new AtomicBoolean(false);
try {
runtime.requestContext().parameters(parameterInfo).run((context) -> {
CdsRequestGlobals requestGlobals = new CdsRequestGlobals(runtime, context.getModel(), unclosedChangeSetTracker);
final String pathInfo = req.getPathInfo();
if (pathInfo == null) {
throw new ErrorStatusException(CdsErrorStatuses.INVALID_URI_RESOURCE);
}
// extract the service from the path
MutablePair serviceInfo = extractServiceInfo(pathInfo);
final ApplicationService applicationService = serviceInfo.getLeft();
fillCdsEntityNamesMap(requestGlobals, applicationService);
final String serviceName = applicationService.getDefinition().getQualifiedName();
final String servicePath = serviceInfo.getRight();
requestGlobals.setApplicationService(applicationService);
LocalizingEdmxProviderWrapper edmxProvider = new LocalizingEdmxProviderWrapper(
runtime.getProvider(EdmxV4Provider.class),
runtime.getProvider(EdmxI18nProvider.class),
locale);
CsdlEdmProvider edm = edmxProvider.getEdmProvider(serviceName);
if (edm == null) {
edm = new CdsServiceEdmProvider(applicationService.getDefinition(),
requestGlobals.getEdmxFlavour());
}
edm = ODataExtendedEdmProvider.wrap(edm);
OData odata = OData.newInstance();
ServiceMetadataETagSupport etagSupport = new SimpleETagSupport(edmxProvider.getETag(serviceName));
ServiceMetadata serviceMetadata = odata.createServiceMetadata(edm, Collections.emptyList(), etagSupport);
requestGlobals.setOData(odata);
requestGlobals.setServiceMetadata(serviceMetadata);
String searchMode = requestGlobals.getRuntime().getEnvironment().getCdsProperties().getOdataV4().getSearchMode();
ODataHttpHandler odataHandler = odata.createHandler(serviceMetadata, Map.of(SearchParser.SEARCH_MODE, searchMode));
odataHandler.register(new OlingoProcessor(requestGlobals));
odataHandler.register(new CustomServiceDocumentProcessor(runtime));
odataHandler.register(new CustomMetadataProcessor(edmxProvider, serviceName));
req.setAttribute("requestMapping", getRequestMappingUrl(req, adapterBasePath, servicePath));
odataHandler.process(req, resp);
});
} catch (ServiceException e) {
int httpStatus = e.getErrorStatus().getHttpStatus();
if(httpStatus >= 500 && httpStatus < 600) {
logger.error(UNEXPECTED_ERROR_MESSAGE, e);
} else {
logger.debug(UNEXPECTED_ERROR_MESSAGE, e);
}
writeErrorResponse(req, resp, httpStatus, e.getLocalizedMessage(locale));
} catch (Exception e) { // NOSONAR
logger.error(UNEXPECTED_ERROR_MESSAGE, e);
writeErrorResponse(req, resp, 500, new ErrorStatusException(ErrorStatuses.SERVER_ERROR).getLocalizedMessage(locale));
} finally {
// safety net for dealing with streaming properties
ChangeSetContext changeSetContext = ChangeSetContext.getCurrent();
if (unclosedChangeSetTracker.get() && changeSetContext != null) {
logger.warn("Closing a detected unclosed ChangeSet Context");
try {
((ChangeSetContextSPI) changeSetContext).close();
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
}
}
@VisibleForTesting
static String getRequestMappingUrl(HttpServletRequest request, String adapterBasePath, String servicePath) {
// Olingo needs to know where the service is on this hostname. We attempt to guess this URL by
// assuming that it will always have contextPath, adapter base path and service name in exact sequence.
// E.g. http://cloud-corporate-canary.somewhere.landscape.corp.sap/test/service/entity/property
// should result in http://cloud-corporate-canary.somewhere.landscape.corp.sap/test/service/
// The `entity/property` part Olingo will figure out by itself
// Escaped slashes in the URL are treated as simple values and not as a segment separator
String contextPath = request.getContextPath();
String originalPath = request.getRequestURI();
// If the URL simply starts with normalized combination of our preconfigured segments, we return it
String newPath = UrlResourcePathBuilder.path(contextPath, adapterBasePath, servicePath).build().getPath();
if (originalPath.startsWith(newPath)) {
return rebuildUrl(request, newPath);
} else {
// Otherwise, we need to ensure that potentially escaped segments in the original URL are same
// as our configured ones
String remainderOfPath = originalPath.substring(contextPath.length());
List result = new ArrayList<>();
result.add(contextPath);
Iterator segments = splitPathSegments(remainderOfPath).iterator();
Stream.concat(splitPathSegments(adapterBasePath), splitPathSegments(servicePath)).forEach(s -> {
if (segments.hasNext()) {
String next = segments.next();
if (s.equals(URLDecoder.decode(next, StandardCharsets.UTF_8))) {
result.add(next);
return;
}
}
throw new ErrorStatusException(CdsErrorStatuses.INVALID_URI_RESOURCE);
});
return rebuildUrl(request, UrlResourcePathBuilder.path(result.toArray(new String[0])).build().getPath());
}
}
private static String rebuildUrl(HttpServletRequest request, String path) {
try {
URI original = URI.create(request.getRequestURL().toString());
return new URI(original.getScheme(), original.getUserInfo(), original.getHost(),
original.getPort(), null, null, null)
.resolve(path) // Do not encode resulting path
.toURL().toString();
} catch (URISyntaxException | MalformedURLException e) {
throw new ServiceException(e);
}
}
private static Stream splitPathSegments(String from) {
return Stream.of(from.split("/")).dropWhile(String::isBlank);
}
private void fillCdsEntityNamesMap(CdsRequestGlobals requestGlobals, ApplicationService applicationService) {
applicationService.getDefinition().entities().forEach(entity -> {
String cdsName = entity.getName();
String edmName = com.sap.cds.services.utils.ODataUtils.toODataName(cdsName);
if (!edmName.equals(cdsName)) {
String key = entity.getQualifier() + "." + edmName;
String value = entity.getQualifier() + "." + cdsName;
requestGlobals.getCdsEntityNames().put(key, value);
}
});
}
private void writeErrorResponse(HttpServletRequest req, HttpServletResponse resp, int httpStatus, String message) throws IOException {
String responseContent = "{\"error\":{\"code\":\"" + httpStatus + "\",\"message\":\"" + message + "\"}}";
resp.setHeader(HttpHeader.CONTENT_TYPE, ContentType.APPLICATION_JSON.toContentTypeString());
resp.setHeader(HttpHeader.ODATA_VERSION, ODataUtils.getODataVersion(req.getHeader(HttpHeader.ODATA_VERSION), req.getHeader(HttpHeader.ODATA_MAX_VERSION)));
resp.setStatus(httpStatus);
resp.getWriter().println(responseContent);
}
/**
* Extracts the service name from the given url path.
*
* Common schema of an odata request:
http://host:port/path/SampleService.svc/Categories(1)/Products?$top=2&$orderby=Name
\______________________________________/\____________________/ \__________________/
| | |
service root URL resource path query options
\________________________________________/
|
pathInfo
*/
private MutablePair extractServiceInfo(String pathInfo) {
if(pathInfo.trim().isEmpty()) {
throw new ErrorStatusException(ErrorStatuses.NOT_FOUND);
}
String path = StringUtils.trim(pathInfo.trim(), '/');
Optional servicePathKey = servicePaths.keySet().stream()
// sort from longest path to shortest
.sorted((a, b) -> -1 * Integer.compare(a.length(), b.length()))
// find the most accurate match
.filter(p -> path.equals(p) || path.startsWith(p + '/'))
.findFirst();
if(servicePathKey.isPresent()) {
// service should have a path derived from the model
String servicePath = servicePathKey.get();
ApplicationService service = servicePaths.get(servicePath);
if (service != null) {
return MutablePair.of(service, servicePath);
}
}
throw new ErrorStatusException(CdsErrorStatuses.SERVICE_NOT_FOUND, path);
}
}