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

com.sap.cds.feature.messaging.eventhub.adapter.EventHubWebhookAdapter Maven / Gradle / Ivy

/**************************************************************************
 * (C) 2019-2024 SAP SE or an SAP affiliate company. All rights reserved. *
 **************************************************************************/
package com.sap.cds.feature.messaging.eventhub.adapter;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.http.HttpStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.sap.cds.feature.messaging.eventhub.service.EventHubMessagingService;
import com.sap.cds.feature.messaging.eventhub.utils.EventHubBindingUtils;
import com.sap.cds.services.ErrorStatuses;
import com.sap.cds.services.ServiceException;
import com.sap.cds.services.messaging.MessagingService;
import com.sap.cds.services.messaging.service.MessagingBrokerQueueListener.MessageAccess;
import com.sap.cds.services.messaging.utils.CloudEventUtils;
import com.sap.cds.services.outbox.OutboxService;
import com.sap.cds.services.request.UserInfo;
import com.sap.cds.services.runtime.CdsRuntime;
import com.sap.cds.services.utils.ErrorStatusException;
import com.sap.cloud.environment.servicebinding.api.ServiceBinding;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

public class EventHubWebhookAdapter extends HttpServlet {

	private static final long serialVersionUID = 1L;
	private static final Logger logger = LoggerFactory.getLogger(EventHubWebhookAdapter.class);
	private static final String UNEXPECTED_ERROR_OCCURRED_MESSAGE = "An unexpected error occurred during servlet processing";

	private final CdsRuntime runtime;
	private final List messagingServices;
	private final String clientId;
	private final boolean isMultitenant;

	public EventHubWebhookAdapter(CdsRuntime runtime) {
		this.runtime = runtime;
		this.messagingServices = runtime.getServiceCatalog().getServices(MessagingService.class)
				.map(OutboxService::unboxed)
				.filter(EventHubMessagingService.class::isInstance)
				.map(EventHubMessagingService.class::cast)
				.toList();
		ServiceBinding binding = EventHubBindingUtils.getServiceBinding(runtime).get();
		this.clientId = EventHubBindingUtils.getClientId(binding);
		this.isMultitenant = EventHubBindingUtils.isBindingMultitenant(binding);
	}

	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		try {
			checkAuthorization(req);

			Message message = new Message(req, isMultitenant);

			for (EventHubMessagingService srv : messagingServices) {
				// only if the receive message topic is subscribed by the service
				if (srv.isRegisteredBrokerTopic(message.getBrokerTopic())) {
					try {
						srv.getQueueListener().receivedMessage(message);
						resp.setStatus(HttpStatus.SC_ACCEPTED);
					} catch (ServiceException exp) {
						if (message.isAcknowledged()) {
							logger.debug("Suppressed exception, as message should be acknowledged", exp);
							resp.setStatus(HttpStatus.SC_ACCEPTED);
						} else {
							throw exp;
						}
					}
				}
			}

		} catch (ServiceException e) {
			int httpStatus = e.getErrorStatus().getHttpStatus();
			if(httpStatus >= 500 && httpStatus < 600) {
				logger.error(UNEXPECTED_ERROR_OCCURRED_MESSAGE, e);
			} else {
				logger.debug(UNEXPECTED_ERROR_OCCURRED_MESSAGE, e);
			}

			writeErrorResponse(req, resp, httpStatus, e.getMessage());
		} catch (Exception e) { // NOSONAR
			logger.error(UNEXPECTED_ERROR_OCCURRED_MESSAGE, e);
			writeErrorResponse(req, resp, 500, new ErrorStatusException(ErrorStatuses.SERVER_ERROR).getMessage());
		}
	}

	private void checkAuthorization(HttpServletRequest req) {
		UserInfo userInfo = runtime.getProvidedUserInfo();
		String azp = (String) userInfo.getAdditionalAttributes().get("azp");
		if(!userInfo.isSystemUser() || azp == null || !azp.equals(clientId)) {
			throw new ErrorStatusException(ErrorStatuses.FORBIDDEN);
		}
	}

	private void writeErrorResponse(HttpServletRequest req, HttpServletResponse resp, int httpStatus, String message) throws IOException {
		String responseContent = "{\"error\":{\"code\":\"" + httpStatus + "\",\"message\":\"" + message + "\"}}";
		resp.setStatus(httpStatus);
		resp.setContentType("application/json");
		resp.getWriter().println(responseContent);
	}

	private static class Message implements MessageAccess {

		private final String id;
		private final String topic;
		private final Map dataMap;
		private final Map headersMap;
		private final String tenant;

		private volatile boolean acknowledged;

		public Message(HttpServletRequest req, boolean isMultiTenant) {
			id = req.getHeader("ce-id");
			topic = req.getHeader("ce-type");

			// only in case of multi-tenant, the tenant header is required
			if(isMultiTenant) {
				tenant = req.getHeader("ce-sapconsumertenant");
			} else {
				tenant = null;
			}

			this.headersMap = new HashMap<>();
			req.getHeaderNames().asIterator().forEachRemaining(h -> {
				if (h.startsWith("ce-")) {
					headersMap.put(h.substring(3), req.getHeader(h));
				}
			});

			logger.debug("Received Event Hub webhook request with type '{}' with ID '{}'", topic, id);
			try {
				String message = new String(req.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
				Map map = CloudEventUtils.toMap(message);
				if (map == null) {
					this.dataMap = new HashMap<>(Map.of("message", message));
				} else {
					this.dataMap = map;
				}
			} catch (IOException e) {
				throw new ServiceException("Failed to read body of webhook request with type '{}' with ID '{}'", topic, id, e);
			}
		}

		@Override
		public String getTenant() {
			return tenant;
		}

		@Override
		public String getId() {
			return id;
		}

		@Override
		public String getMessage() {
			throw new IllegalStateException();
		}

		@Override
		public Map getDataMap() {
			return this.dataMap;
		}

		@Override
		public Map getHeadersMap() {
			return this.headersMap;
		}

		@Override
		public String getBrokerTopic() {
			return topic;
		}

		@Override
		public void acknowledge() {
			acknowledged = true;
		}

		public boolean isAcknowledged() {
			return acknowledged;
		}
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy