
com.sap.cds.feature.messaging.em.mt.webhook.EnterpriseMessagingWebhookAdapter Maven / Gradle / Ivy
/**************************************************************************
* (C) 2019-2024 SAP SE or an SAP affiliate company. All rights reserved. *
**************************************************************************/
package com.sap.cds.feature.messaging.em.mt.webhook;
import static com.sap.cds.services.messaging.utils.MessagingUtils.toStructuredMessage;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.function.Consumer;
import org.apache.http.HttpStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.google.common.io.CharStreams;
import com.sap.cds.feature.messaging.em.mt.service.EnterpriseMessagingMtService;
import com.sap.cds.feature.messaging.em.mt.service.EnterpriseMessagingTenantStatus;
import com.sap.cds.impl.util.Pair;
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;
import com.sap.cds.services.messaging.service.MessagingBrokerQueueListener.MessageAccess;
import com.sap.cds.services.mt.TenantProviderService;
import com.sap.cds.services.outbox.OutboxService;
import com.sap.cds.services.request.RequestContext;
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 jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
public class EnterpriseMessagingWebhookAdapter extends HttpServlet {
private static final long serialVersionUID = 1L;
private static final Logger logger = LoggerFactory.getLogger(EnterpriseMessagingWebhookAdapter.class);
private static final String HEADER_HANDSHAKE_FROM = "webhook-request-origin";
private static final String HEADER_HANDSHAKE_TO = "webhook-allowed-origin";
private static final String HEADER_QUEUE = "x-queue";
private static final String HEADER_TOPIC = "x-address";
private static final String ROLE_EMCALLBACK = "emcallback";
private static final String ROLE_EMMANAGEMENT = "emmanagement";
private static final String UNEXPECTED_ERROR_OCCURRED_MESSAGE = "An unexpected error occurred during servlet processing";
private final CdsRuntime runtime;
private final Map queueListeners = new HashMap<>();
private final TenantProviderService tenantService;
private final JsonMapper mapper = new JsonMapper();
public EnterpriseMessagingWebhookAdapter(CdsRuntime runtime) {
this.runtime = runtime;
// get all EM messaging MT services in order to register the queueListeners
runtime.getServiceCatalog().getServices(MessagingService.class).map(OutboxService::unboxed)
.filter(EnterpriseMessagingMtService.class::isInstance).map(EnterpriseMessagingMtService.class::cast).forEach(service -> {
MessagingBrokerQueueListener listener = service.getQueueListener();
this.queueListeners.put(listener.getQueueName(), listener);
logger.info("Registered webhook queue listener for '{}'", listener.getQueueName());
});
tenantService = runtime.getServiceCatalog().getService(TenantProviderService.class, TenantProviderService.DEFAULT_NAME);
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
runInRequestContext(req, resp, requestContext -> {
checkRole(ROLE_EMMANAGEMENT);
String tenantId = getUrlParameterTenantId(req);
boolean verbose = Boolean.parseBoolean(req.getParameter("verbose"));
if (tenantId != null) {
runtime.requestContext().systemUser(tenantId).run(tenantRequestContext -> {
writeJsonResponse(req, resp, getTenantStatus(tenantId, verbose));
});
} else {
List tenants = new ArrayList<>();
tenantService.readTenants()
.forEach(tenant -> runtime.requestContext().systemUser(tenant).run(tenantRequestContext -> {
tenants.add(getTenantStatus(tenant, verbose));
}));
writeJsonResponse(req, resp, tenants);
}
});
}
private EnterpriseMessagingTenantStatus getTenantStatus(String tenantId, boolean verbose) {
List statuses = runtime.getServiceCatalog()
.getServices(MessagingService.class)
.map(OutboxService::unboxed)
.filter(EnterpriseMessagingMtService.class::isInstance)
.map(EnterpriseMessagingMtService.class::cast)
.map(s -> s.getTenantStatus(tenantId, verbose))
.toList();
EnterpriseMessagingTenantStatus merged = new EnterpriseMessagingTenantStatus(tenantId);
for(EnterpriseMessagingTenantStatus status : statuses) {
merged.getServices().putAll(status.getServices());
merged.getUnmanagedQueues().addAll(status.getUnmanagedQueues());
merged.getUnmanagedWebhooks().addAll(status.getUnmanagedWebhooks());
}
// filter unmanaged queues managed by a different service
merged.getServices().values().forEach(s -> merged.getUnmanagedQueues().remove(s.getQueue()));
// filter unmanaged webhooks managed by a different service
merged.getServices().values().forEach(s -> s.getWebhooks().forEach(w -> merged.getUnmanagedWebhooks().remove(w)));
return merged;
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
runWithoutRequestContext(req, resp, () -> {
checkRole(ROLE_EMCALLBACK);
Message message = new Message(req, runtime.getProvidedUserInfo().getTenant());
MessagingBrokerQueueListener listener = this.queueListeners.get(message.getBrokerQueue());
if (listener == null) {
throw new ServiceException(ErrorStatuses.NOT_FOUND, "Received webhook request on unknown queue '{}' with topic '{}' and ID '{}'",
message.getBrokerQueue(), message.getBrokerTopic(), message.getId());
}
try {
listener.receivedMessage(message);
resp.setStatus(HttpStatus.SC_ACCEPTED);
} catch (ServiceException exp) {
if(message.isAcknowledged()) {
logger.debug("Ignored the exception as accepted by the error handler", exp);
resp.setStatus(HttpStatus.SC_ACCEPTED);
} else {
throw exp;
}
}
});
}
@Override
protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
runInRequestContext(req, resp, requestContext -> {
checkRole(ROLE_EMCALLBACK);
// EM webhook registration handshake
String origin = req.getHeader(HEADER_HANDSHAKE_FROM);
if (origin == null) {
throw new ServiceException(ErrorStatuses.BAD_REQUEST, "Received invalid webhook handshake without origin");
} else {
resp.setHeader(HEADER_HANDSHAKE_TO, origin); // NOSONAR
logger.info("Webhook registration handshake with origin '{}' received", origin);
resp.setStatus(HttpStatus.SC_ACCEPTED);
}
});
}
private void runInRequestContext(HttpServletRequest req, HttpServletResponse resp, Consumer consumer) throws IOException {
runWithoutRequestContext(req, resp, () -> {
// open request context
runtime.requestContext().systemUserProvider().run(consumer);
});
}
private void runWithoutRequestContext(HttpServletRequest req, HttpServletResponse resp, Runnable action) throws IOException {
// extract the locale according to request parameters
Locale locale = runtime.getProvidedParameterInfo().getLocale();
try {
action.run();
} 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.getLocalizedMessage(locale));
} catch (Exception e) { // NOSONAR
logger.error(UNEXPECTED_ERROR_OCCURRED_MESSAGE, e);
writeErrorResponse(req, resp, 500, new ErrorStatusException(ErrorStatuses.SERVER_ERROR).getLocalizedMessage(locale));
}
}
private void checkRole(String role) {
if (!runtime.getProvidedUserInfo().hasRole(role)) {
throw new ErrorStatusException(CdsErrorStatuses.TENANT_ADMIN_FORBIDDEN);
}
}
private String getUrlParameterTenantId(HttpServletRequest req) {
String[] pathParameters = StringUtils.trim(req.getPathInfo(), '/').split("/");
return pathParameters.length > 0 ? (StringUtils.isEmpty(pathParameters[0]) ? null : pathParameters[0]) : null;
}
private void writeJsonResponse(HttpServletRequest req, HttpServletResponse resp, Object content) {
resp.setContentType("application/json");
try {
PrintWriter out = resp.getWriter();
mapper.writeValue(out, content);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
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 String id;
private final String queue;
private final String topic;
private final String message;
private final String tenant;
private volatile boolean acknowledged;
private Map dataMap;
private Map headersMap;
public Message(HttpServletRequest req, String tenant) {
String theQueue = req.getHeader(HEADER_QUEUE);
if(theQueue != null) {
theQueue = theQueue.trim();
}
queue = theQueue;
this.tenant = tenant;
String theTopic = req.getHeader(HEADER_TOPIC);
if (theTopic != null) {
theTopic = theTopic.trim();
if (theTopic.startsWith("topic:")) {
theTopic = theTopic.substring(6).trim();
}
} else {
theTopic = queue;
}
topic = theTopic;
logger.debug("Received webhook request from queue '{}' on topic '{}' with ID '{}'", queue, topic, id);
try (Reader reader = new InputStreamReader(req.getInputStream(), StandardCharsets.UTF_8)) {
this.message = CharStreams.toString(reader);
} catch (IOException e) {
throw new ServiceException("Failed to read body of webhook request for queue '{}'", queue, e);
}
}
@Override
public String getId() {
return id;
}
@Override
public String getTenant() {
return tenant;
}
@Override
public String getMessage() {
return message;
}
@Override
public Map getDataMap() {
if (this.dataMap == null) {
populateMaps();
}
return this.dataMap;
}
@Override
public Map getHeadersMap() {
if (this.headersMap == null) {
populateMaps();
}
return this.headersMap;
}
private void populateMaps() {
Pair
© 2015 - 2025 Weber Informatics LLC | Privacy Policy