All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sap.cds.adapter.odata.v4.CdsODataV4Servlet Maven / Gradle / Ivy

There is a newer version: 3.2.0
Show newest version
/**************************************************************************
 * (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);
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy