Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/pull-request-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5

- name: Spotless check
run: mvn spotless:check -Dspotless.check.skip=false

- name: Build
uses: ./.github/actions/build
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,6 @@
*/
package com.sap.cds.feature.attachments.configuration;

import java.time.Duration;
import java.util.Optional;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.sap.cds.feature.attachments.handler.applicationservice.CreateAttachmentsHandler;
import com.sap.cds.feature.attachments.handler.applicationservice.DeleteAttachmentsHandler;
import com.sap.cds.feature.attachments.handler.applicationservice.ReadAttachmentsHandler;
Expand Down Expand Up @@ -50,134 +44,162 @@
import com.sap.cds.services.runtime.CdsRuntimeConfigurer;
import com.sap.cds.services.utils.environment.ServiceBindingUtils;
import com.sap.cloud.environment.servicebinding.api.ServiceBinding;
import java.time.Duration;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* The class {@link Registration} is a configuration class that registers the services and event handlers for the
* attachments feature.
* The class {@link Registration} is a configuration class that registers the services and event
* handlers for the attachments feature.
*/
public class Registration implements CdsRuntimeConfiguration {

private static final Logger logger = LoggerFactory.getLogger(Registration.class);

@Override
public void services(CdsRuntimeConfigurer configurer) {
configurer.service(new AttachmentsServiceImpl());
}

@Override
public void eventHandlers(CdsRuntimeConfigurer configurer) {
logger.debug("Registering event handlers");

CdsRuntime runtime = configurer.getCdsRuntime();
ServiceCatalog serviceCatalog = runtime.getServiceCatalog();

// get required services from the service catalog
PersistenceService persistenceService = serviceCatalog.getService(PersistenceService.class,
PersistenceService.DEFAULT_NAME);
AttachmentService attachmentService = serviceCatalog.getService(AttachmentService.class,
AttachmentService.DEFAULT_NAME);

// outbox AttachmentService if OutboxService is available
OutboxService outboxService = serviceCatalog.getService(OutboxService.class,
OutboxService.PERSISTENT_UNORDERED_NAME);
AttachmentService outboxedAttachmentService;
if (outboxService != null) {
outboxedAttachmentService = outboxService.outboxed(attachmentService);
} else {
outboxedAttachmentService = attachmentService;
logger.warn("OutboxService '{}' is not available. AttachmentService will not be outboxed.",
OutboxService.PERSISTENT_UNORDERED_NAME);
}

// build malware scanner client, could be null if no service binding is available
MalwareScanClient scanClient = buildMalwareScanClient(runtime.getEnvironment());

AttachmentMalwareScanner malwareScanner = new DefaultAttachmentMalwareScanner(persistenceService,
attachmentService, scanClient);

EndTransactionMalwareScanProvider malwareScanEndTransactionListener = (attachmentEntity,
contentId) -> new EndTransactionMalwareScanRunner(attachmentEntity, contentId, malwareScanner, runtime);

// register event handlers for attachment service
configurer.eventHandler(new DefaultAttachmentsServiceHandler(malwareScanEndTransactionListener));

MarkAsDeletedAttachmentEvent deleteEvent = new MarkAsDeletedAttachmentEvent(outboxedAttachmentService);
ModifyAttachmentEventFactory eventFactory = buildAttachmentEventFactory(attachmentService, deleteEvent,
outboxedAttachmentService);
AttachmentsReader attachmentsReader = new AttachmentsReader(new AssociationCascader(), persistenceService);
ThreadLocalDataStorage storage = new ThreadLocalDataStorage();

// register event handlers for application service, if at least one application service is available
boolean hasApplicationServices = serviceCatalog.getServices(ApplicationService.class).findFirst().isPresent();
if (hasApplicationServices) {
configurer.eventHandler(new CreateAttachmentsHandler(eventFactory, storage));
configurer.eventHandler(
new UpdateAttachmentsHandler(eventFactory, attachmentsReader, outboxedAttachmentService, storage));
configurer.eventHandler(new DeleteAttachmentsHandler(attachmentsReader, deleteEvent));
EndTransactionMalwareScanRunner scanRunner = new EndTransactionMalwareScanRunner(null, null, malwareScanner,
runtime);
configurer.eventHandler(
new ReadAttachmentsHandler(attachmentService, new AttachmentStatusValidator(), scanRunner));
} else {
logger.debug(
"No application service is available. Application service event handlers will not be registered.");
}

// register event handlers on draft service, if at least one draft service is available
boolean hasDraftServices = serviceCatalog.getServices(DraftService.class).findFirst().isPresent();
if (hasDraftServices) {
configurer.eventHandler(new DraftPatchAttachmentsHandler(persistenceService, eventFactory));
configurer.eventHandler(new DraftCancelAttachmentsHandler(attachmentsReader, deleteEvent));
configurer.eventHandler(new DraftActiveAttachmentsHandler(storage));
} else {
logger.debug("No draft service is available. Draft event handlers will not be registered.");
}
}

/**
* Builds the {@link MalwareScanClient malware scanner client} based on the service binding.
*
* @param environment the {@link CdsEnvironment environment} to retrieve the service binding from
* @return the {@link MalwareScanClient malware scanner client} or {@code null} if no service binding is available
*/
private static MalwareScanClient buildMalwareScanClient(CdsEnvironment environment) {
// retrieve the service binding for the malware scanner service
Optional<ServiceBinding> bindingOpt = environment.getServiceBindings()
.filter(b -> ServiceBindingUtils.matches(b, DefaultAttachmentMalwareScanner.MALWARE_SCAN_SERVICE_LABEL))
.findFirst();

if (bindingOpt.isPresent()) {
ServiceBinding binding = bindingOpt.get();
ConnectionPool connectionPool = getConnectionPool(environment);
HttpClientProvider clientProvider = new MalwareScanClientProvider(binding, connectionPool);
if (logger.isInfoEnabled()) {
logger.info(
"Using Malware Scanning service binding with name '{}' and plan '{}' for malware scanning of attachments.",
binding.getName().orElse("unknown"), binding.getServicePlan().orElse("unknown"));
}
return new DefaultMalwareScanClient(clientProvider);
}

logger.info("No Malware Scanning service binding found, malware scanning is disabled.");
return null;
}

private static ModifyAttachmentEventFactory buildAttachmentEventFactory(AttachmentService attachmentService,
MarkAsDeletedAttachmentEvent deleteEvent, AttachmentService outboxedAttachmentService) {
ListenerProvider listenerProvider = (contentId, cdsRuntime) -> new CreationChangeSetListener(contentId,
cdsRuntime, outboxedAttachmentService);
CreateAttachmentEvent createEvent = new CreateAttachmentEvent(attachmentService, listenerProvider);
UpdateAttachmentEvent updateEvent = new UpdateAttachmentEvent(createEvent, deleteEvent);
DoNothingAttachmentEvent doNothingEvent = new DoNothingAttachmentEvent();
return new ModifyAttachmentEventFactory(createEvent, updateEvent, deleteEvent, doNothingEvent);
}

private static ConnectionPool getConnectionPool(CdsEnvironment env) {
// the common prefix for the connection pool configuration
final String prefix = "cds.attachments.malwareScanner.http.%s";
Duration timeout = Duration.ofSeconds(env.getProperty(prefix.formatted("timeout"), Integer.class, 120));
int maxConnections = env.getProperty(prefix.formatted("maxConnections"), Integer.class, 20);
logger.debug("Connection pool configuration: timeout={}, maxConnections={}", timeout, maxConnections);
return new ConnectionPool(timeout, maxConnections, maxConnections);
}
private static final Logger logger = LoggerFactory.getLogger(Registration.class);

@Override
public void services(CdsRuntimeConfigurer configurer) {
configurer.service(new AttachmentsServiceImpl());
}

@Override
public void eventHandlers(CdsRuntimeConfigurer configurer) {
logger.debug("Registering event handlers");

CdsRuntime runtime = configurer.getCdsRuntime();
ServiceCatalog serviceCatalog = runtime.getServiceCatalog();

// get required services from the service catalog
PersistenceService persistenceService =
serviceCatalog.getService(PersistenceService.class, PersistenceService.DEFAULT_NAME);
AttachmentService attachmentService =
serviceCatalog.getService(AttachmentService.class, AttachmentService.DEFAULT_NAME);

// outbox AttachmentService if OutboxService is available
OutboxService outboxService =
serviceCatalog.getService(OutboxService.class, OutboxService.PERSISTENT_UNORDERED_NAME);
AttachmentService outboxedAttachmentService;
if (outboxService != null) {
outboxedAttachmentService = outboxService.outboxed(attachmentService);
} else {
outboxedAttachmentService = attachmentService;
logger.warn(
"OutboxService '{}' is not available. AttachmentService will not be outboxed.",
OutboxService.PERSISTENT_UNORDERED_NAME);
}

// build malware scanner client, could be null if no service binding is available
MalwareScanClient scanClient = buildMalwareScanClient(runtime.getEnvironment());

AttachmentMalwareScanner malwareScanner =
new DefaultAttachmentMalwareScanner(persistenceService, attachmentService, scanClient);

EndTransactionMalwareScanProvider malwareScanEndTransactionListener =
(attachmentEntity, contentId) ->
new EndTransactionMalwareScanRunner(
attachmentEntity, contentId, malwareScanner, runtime);

// register event handlers for attachment service
configurer.eventHandler(
new DefaultAttachmentsServiceHandler(malwareScanEndTransactionListener));

MarkAsDeletedAttachmentEvent deleteEvent =
new MarkAsDeletedAttachmentEvent(outboxedAttachmentService);
ModifyAttachmentEventFactory eventFactory =
buildAttachmentEventFactory(attachmentService, deleteEvent, outboxedAttachmentService);
AttachmentsReader attachmentsReader =
new AttachmentsReader(new AssociationCascader(), persistenceService);
ThreadLocalDataStorage storage = new ThreadLocalDataStorage();

// register event handlers for application service, if at least one application service is
// available
boolean hasApplicationServices =
serviceCatalog.getServices(ApplicationService.class).findFirst().isPresent();
if (hasApplicationServices) {
configurer.eventHandler(new CreateAttachmentsHandler(eventFactory, storage));
configurer.eventHandler(
new UpdateAttachmentsHandler(
eventFactory, attachmentsReader, outboxedAttachmentService, storage));
configurer.eventHandler(new DeleteAttachmentsHandler(attachmentsReader, deleteEvent));
EndTransactionMalwareScanRunner scanRunner =
new EndTransactionMalwareScanRunner(null, null, malwareScanner, runtime);
configurer.eventHandler(
new ReadAttachmentsHandler(
attachmentService, new AttachmentStatusValidator(), scanRunner));
} else {
logger.debug(
"No application service is available. Application service event handlers will not be registered.");
}

// register event handlers on draft service, if at least one draft service is available
boolean hasDraftServices =
serviceCatalog.getServices(DraftService.class).findFirst().isPresent();
if (hasDraftServices) {
configurer.eventHandler(new DraftPatchAttachmentsHandler(persistenceService, eventFactory));
configurer.eventHandler(new DraftCancelAttachmentsHandler(attachmentsReader, deleteEvent));
configurer.eventHandler(new DraftActiveAttachmentsHandler(storage));
} else {
logger.debug("No draft service is available. Draft event handlers will not be registered.");
}
}

/**
* Builds the {@link MalwareScanClient malware scanner client} based on the service binding.
*
* @param environment the {@link CdsEnvironment environment} to retrieve the service binding from
* @return the {@link MalwareScanClient malware scanner client} or {@code null} if no service
* binding is available
*/
private static MalwareScanClient buildMalwareScanClient(CdsEnvironment environment) {
// retrieve the service binding for the malware scanner service
Optional<ServiceBinding> bindingOpt =
environment
.getServiceBindings()
.filter(
b ->
ServiceBindingUtils.matches(
b, DefaultAttachmentMalwareScanner.MALWARE_SCAN_SERVICE_LABEL))
.findFirst();

if (bindingOpt.isPresent()) {
ServiceBinding binding = bindingOpt.get();
ConnectionPool connectionPool = getConnectionPool(environment);
HttpClientProvider clientProvider = new MalwareScanClientProvider(binding, connectionPool);
if (logger.isInfoEnabled()) {
logger.info(
"Using Malware Scanning service binding with name '{}' and plan '{}' for malware scanning of attachments.",
binding.getName().orElse("unknown"),
binding.getServicePlan().orElse("unknown"));
}
return new DefaultMalwareScanClient(clientProvider);
}

logger.info("No Malware Scanning service binding found, malware scanning is disabled.");
return null;
}

private static ModifyAttachmentEventFactory buildAttachmentEventFactory(
AttachmentService attachmentService,
MarkAsDeletedAttachmentEvent deleteEvent,
AttachmentService outboxedAttachmentService) {
ListenerProvider listenerProvider =
(contentId, cdsRuntime) ->
new CreationChangeSetListener(contentId, cdsRuntime, outboxedAttachmentService);
CreateAttachmentEvent createEvent =
new CreateAttachmentEvent(attachmentService, listenerProvider);
UpdateAttachmentEvent updateEvent = new UpdateAttachmentEvent(createEvent, deleteEvent);
DoNothingAttachmentEvent doNothingEvent = new DoNothingAttachmentEvent();
return new ModifyAttachmentEventFactory(createEvent, updateEvent, deleteEvent, doNothingEvent);
}

private static ConnectionPool getConnectionPool(CdsEnvironment env) {
// the common prefix for the connection pool configuration
final String prefix = "cds.attachments.malwareScanner.http.%s";
Duration timeout =
Duration.ofSeconds(env.getProperty(prefix.formatted("timeout"), Integer.class, 120));
int maxConnections = env.getProperty(prefix.formatted("maxConnections"), Integer.class, 20);
logger.debug(
"Connection pool configuration: timeout={}, maxConnections={}", timeout, maxConnections);
return new ConnectionPool(timeout, maxConnections, maxConnections);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,6 @@

import static java.util.Objects.requireNonNull;

import java.util.ArrayList;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.sap.cds.CdsData;
import com.sap.cds.feature.attachments.handler.applicationservice.helper.ModifyApplicationHandlerHelper;
import com.sap.cds.feature.attachments.handler.applicationservice.helper.ReadonlyDataContextEnhancer;
Expand All @@ -24,38 +18,46 @@
import com.sap.cds.services.handler.annotations.HandlerOrder;
import com.sap.cds.services.handler.annotations.ServiceName;
import com.sap.cds.services.utils.OrderConstants;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* The class {@link CreateAttachmentsHandler} is an event handler that is responsible for creating attachments for
* entities. It is called before a create event is executed.
* The class {@link CreateAttachmentsHandler} is an event handler that is responsible for creating
* attachments for entities. It is called before a create event is executed.
*/
@ServiceName(value = "*", type = ApplicationService.class)
public class CreateAttachmentsHandler implements EventHandler {

private static final Logger logger = LoggerFactory.getLogger(CreateAttachmentsHandler.class);

private final ModifyAttachmentEventFactory eventFactory;
private final ThreadDataStorageReader storageReader;

public CreateAttachmentsHandler(ModifyAttachmentEventFactory eventFactory, ThreadDataStorageReader storageReader) {
this.eventFactory = requireNonNull(eventFactory, "eventFactory must not be null");
this.storageReader = requireNonNull(storageReader, "storageReader must not be null");
}

@Before
@HandlerOrder(OrderConstants.Before.CHECK_CAPABILITIES)
void processBeforeForDraft(CdsCreateEventContext context, List<CdsData> data) {
// before the attachment's readonly fields are removed by the runtime, preserve them in a custom field in data
ReadonlyDataContextEnhancer.preserveReadonlyFields(context.getTarget(), data, storageReader.get());
}

@Before
@HandlerOrder(HandlerOrder.LATE)
void processBefore(CdsCreateEventContext context, List<CdsData> data) {
if (ApplicationHandlerHelper.containsContentField(context.getTarget(), data)) {
logger.debug("Processing before {} event for entity {}", context.getEvent(), context.getTarget());
ModifyApplicationHandlerHelper.handleAttachmentForEntities(context.getTarget(), data, new ArrayList<>(),
eventFactory, context);
}
}
private static final Logger logger = LoggerFactory.getLogger(CreateAttachmentsHandler.class);

private final ModifyAttachmentEventFactory eventFactory;
private final ThreadDataStorageReader storageReader;

public CreateAttachmentsHandler(
ModifyAttachmentEventFactory eventFactory, ThreadDataStorageReader storageReader) {
this.eventFactory = requireNonNull(eventFactory, "eventFactory must not be null");
this.storageReader = requireNonNull(storageReader, "storageReader must not be null");
}

@Before
@HandlerOrder(OrderConstants.Before.CHECK_CAPABILITIES)
void processBeforeForDraft(CdsCreateEventContext context, List<CdsData> data) {
// before the attachment's readonly fields are removed by the runtime, preserve them in a custom
// field in data
ReadonlyDataContextEnhancer.preserveReadonlyFields(
context.getTarget(), data, storageReader.get());
}

@Before
@HandlerOrder(HandlerOrder.LATE)
void processBefore(CdsCreateEventContext context, List<CdsData> data) {
if (ApplicationHandlerHelper.containsContentField(context.getTarget(), data)) {
logger.debug(
"Processing before {} event for entity {}", context.getEvent(), context.getTarget());
ModifyApplicationHandlerHelper.handleAttachmentForEntities(
context.getTarget(), data, new ArrayList<>(), eventFactory, context);
}
}
}
Loading
Loading