Skip to content

Commit 9680ab6

Browse files
committed
feat: add @Validation.MinItems and @Validation.MaxItems support for attachment compositions
Introduces item-count validation for attachment compositions annotated with @Validation.MinItems and @Validation.MaxItems. In draft mode violations produce warnings (draft allows an invalid state); on active entities and during draft activation (DRAFT_SAVE) they produce errors. - ItemCountValidationHelper: shared utility that reads annotations, counts items in the payload, and issues warn/error messages targeting the composition table so Fiori elements can highlight it. Supports integer values, and the i18n override pattern (attachment_minItems_{Entity}_{property}) for per-property message customisation. - ItemsCountValidationHandler: @before LATE on ApplicationService CREATE and UPDATE; detects draft state via IsActiveEntity=false in payload. - DraftSaveItemsCountValidationHandler: @before LATE on DraftService DRAFT_SAVE; always errors since activation must enforce constraints. - Registration: registers both new handlers alongside the existing ones. - i18n: adds attachment_minItems and attachment_maxItems message keys.
1 parent edbcfc8 commit 9680ab6

9 files changed

Lines changed: 579 additions & 2 deletions

File tree

cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/configuration/Registration.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import com.sap.cds.feature.attachments.handler.applicationservice.CreateAttachmentsHandler;
77
import com.sap.cds.feature.attachments.handler.applicationservice.DeleteAttachmentsHandler;
8+
import com.sap.cds.feature.attachments.handler.applicationservice.ItemsCountValidationHandler;
89
import com.sap.cds.feature.attachments.handler.applicationservice.ReadAttachmentsHandler;
910
import com.sap.cds.feature.attachments.handler.applicationservice.UpdateAttachmentsHandler;
1011
import com.sap.cds.feature.attachments.handler.applicationservice.helper.ModifyApplicationHandlerHelper;
@@ -22,6 +23,7 @@
2223
import com.sap.cds.feature.attachments.handler.draftservice.DraftActiveAttachmentsHandler;
2324
import com.sap.cds.feature.attachments.handler.draftservice.DraftCancelAttachmentsHandler;
2425
import com.sap.cds.feature.attachments.handler.draftservice.DraftPatchAttachmentsHandler;
26+
import com.sap.cds.feature.attachments.handler.draftservice.DraftSaveItemsCountValidationHandler;
2527
import com.sap.cds.feature.attachments.service.AttachmentService;
2628
import com.sap.cds.feature.attachments.service.AttachmentsServiceImpl;
2729
import com.sap.cds.feature.attachments.service.handler.DefaultAttachmentsServiceHandler;
@@ -133,6 +135,7 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) {
133135
configurer.eventHandler(
134136
new ReadAttachmentsHandler(
135137
attachmentService, new AttachmentStatusValidator(), scanRunner));
138+
configurer.eventHandler(new ItemsCountValidationHandler());
136139
} else {
137140
logger.debug(
138141
"No application service is available. Application service event handlers will not be registered.");
@@ -146,6 +149,7 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) {
146149
new DraftPatchAttachmentsHandler(persistenceService, eventFactory, defaultMaxSize));
147150
configurer.eventHandler(new DraftCancelAttachmentsHandler(attachmentsReader, deleteEvent));
148151
configurer.eventHandler(new DraftActiveAttachmentsHandler(storage));
152+
configurer.eventHandler(new DraftSaveItemsCountValidationHandler());
149153
} else {
150154
logger.debug("No draft service is available. Draft event handlers will not be registered.");
151155
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* © 2024-2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors.
3+
*/
4+
package com.sap.cds.feature.attachments.handler.applicationservice;
5+
6+
import com.sap.cds.CdsData;
7+
import com.sap.cds.feature.attachments.handler.common.ItemCountValidationHelper;
8+
import com.sap.cds.services.cds.ApplicationService;
9+
import com.sap.cds.services.cds.CdsCreateEventContext;
10+
import com.sap.cds.services.cds.CdsUpdateEventContext;
11+
import com.sap.cds.services.handler.EventHandler;
12+
import com.sap.cds.services.handler.annotations.Before;
13+
import com.sap.cds.services.handler.annotations.HandlerOrder;
14+
import com.sap.cds.services.handler.annotations.ServiceName;
15+
import java.util.List;
16+
17+
/**
18+
* The class {@link ItemsCountValidationHandler} validates {@code @Validation.MinItems} and
19+
* {@code @Validation.MaxItems} annotations on attachment compositions during CREATE and UPDATE
20+
* events. In draft mode (entity not yet activated) a warning is issued; on active entities an error
21+
* is raised.
22+
*/
23+
@ServiceName(value = "*", type = ApplicationService.class)
24+
public class ItemsCountValidationHandler implements EventHandler {
25+
26+
@Before
27+
@HandlerOrder(HandlerOrder.LATE)
28+
void processCreateBefore(CdsCreateEventContext context, List<CdsData> data) {
29+
boolean isDraft = isDraftData(data);
30+
ItemCountValidationHelper.validateItemCounts(
31+
context.getTarget(), data, isDraft, context.getMessages());
32+
}
33+
34+
@Before
35+
@HandlerOrder(HandlerOrder.LATE)
36+
void processUpdateBefore(CdsUpdateEventContext context, List<CdsData> data) {
37+
boolean isDraft = isDraftData(data);
38+
ItemCountValidationHelper.validateItemCounts(
39+
context.getTarget(), data, isDraft, context.getMessages());
40+
}
41+
42+
private boolean isDraftData(List<CdsData> data) {
43+
return data.stream().anyMatch(d -> Boolean.FALSE.equals(d.get("IsActiveEntity")));
44+
}
45+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* © 2024-2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors.
3+
*/
4+
package com.sap.cds.feature.attachments.handler.common;
5+
6+
import com.sap.cds.CdsData;
7+
import com.sap.cds.reflect.CdsEntity;
8+
import com.sap.cds.services.messages.Messages;
9+
import java.util.List;
10+
11+
public final class ItemCountValidationHelper {
12+
13+
private static final String MIN_ITEMS_KEY = "attachment_minItems";
14+
private static final String MAX_ITEMS_KEY = "attachment_maxItems";
15+
16+
public static void validateItemCounts(
17+
CdsEntity entity, List<? extends CdsData> data, boolean isDraft, Messages messages) {
18+
entity
19+
.compositions()
20+
.forEach(
21+
composition -> {
22+
String compositionName = composition.getName();
23+
24+
// only validate if the composition was included in the payload
25+
boolean presentInPayload =
26+
data.stream().anyMatch(d -> d.containsKey(compositionName));
27+
if (!presentInPayload) {
28+
return;
29+
}
30+
31+
long count =
32+
data.stream()
33+
.filter(d -> d.containsKey(compositionName))
34+
.mapToLong(
35+
d -> {
36+
Object val = d.get(compositionName);
37+
return val instanceof List<?> list ? list.size() : 0L;
38+
})
39+
.sum();
40+
41+
composition
42+
.<Object>findAnnotation("Validation.MinItems")
43+
.ifPresent(
44+
annotation -> {
45+
Object value = annotation.getValue();
46+
if (value instanceof Boolean)
47+
return; // bare annotation without value → skip
48+
int limit = ((Number) value).intValue();
49+
if (count < limit) {
50+
String msgKey =
51+
MIN_ITEMS_KEY + "_" + entity.getName() + "_" + compositionName;
52+
issueMessage(
53+
isDraft,
54+
messages,
55+
msgKey,
56+
limit,
57+
entity.getQualifiedName(),
58+
compositionName);
59+
}
60+
});
61+
62+
composition
63+
.<Object>findAnnotation("Validation.MaxItems")
64+
.ifPresent(
65+
annotation -> {
66+
Object value = annotation.getValue();
67+
if (value instanceof Boolean)
68+
return; // bare annotation without value → skip
69+
int limit = ((Number) value).intValue();
70+
if (count > limit) {
71+
String msgKey =
72+
MAX_ITEMS_KEY + "_" + entity.getName() + "_" + compositionName;
73+
issueMessage(
74+
isDraft,
75+
messages,
76+
msgKey,
77+
limit,
78+
entity.getQualifiedName(),
79+
compositionName);
80+
}
81+
});
82+
});
83+
}
84+
85+
private static void issueMessage(
86+
boolean isDraft,
87+
Messages messages,
88+
String msgKey,
89+
int limit,
90+
String entityQualifiedName,
91+
String compositionName) {
92+
if (isDraft) {
93+
messages.warn(msgKey, limit).target(entityQualifiedName, e -> e.to(compositionName));
94+
} else {
95+
messages.error(msgKey, limit).target(entityQualifiedName, e -> e.to(compositionName));
96+
}
97+
}
98+
99+
private ItemCountValidationHelper() {
100+
// avoid instantiation
101+
}
102+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* © 2024-2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors.
3+
*/
4+
package com.sap.cds.feature.attachments.handler.draftservice;
5+
6+
import com.sap.cds.CdsData;
7+
import com.sap.cds.feature.attachments.handler.common.ItemCountValidationHelper;
8+
import com.sap.cds.services.draft.DraftSaveEventContext;
9+
import com.sap.cds.services.draft.DraftService;
10+
import com.sap.cds.services.handler.EventHandler;
11+
import com.sap.cds.services.handler.annotations.Before;
12+
import com.sap.cds.services.handler.annotations.HandlerOrder;
13+
import com.sap.cds.services.handler.annotations.ServiceName;
14+
import java.util.List;
15+
16+
/**
17+
* The class {@link DraftSaveItemsCountValidationHandler} validates {@code @Validation.MinItems} and
18+
* {@code @Validation.MaxItems} annotations on attachment compositions when a draft is saved
19+
* (activated). As draft activation transitions to an active entity, violations always raise errors.
20+
*/
21+
@ServiceName(value = "*", type = DraftService.class)
22+
public class DraftSaveItemsCountValidationHandler implements EventHandler {
23+
24+
@Before
25+
@HandlerOrder(HandlerOrder.LATE)
26+
void processBeforeDraftSave(DraftSaveEventContext context, List<CdsData> data) {
27+
// isDraft=false: saving a draft to active must enforce the constraint as an error
28+
ItemCountValidationHelper.validateItemCounts(
29+
context.getTarget(), data, false, context.getMessages());
30+
}
31+
}

cds-feature-attachments/src/main/resources/cds/com.sap.cds/cds-feature-attachments/_i18n/i18n.properties

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,7 @@ attachment_note=Note
3636
attachment=Attachment
3737
#XTIT: Header label for Attachments
3838
attachments=Attachments
39+
#XMSG: Error message when too few attachments are provided (min items validation)
40+
attachment_minItems=A minimum of {0} attachments is required.
41+
#XMSG: Error message when too many attachments are provided (max items validation)
42+
attachment_maxItems=A maximum of {0} attachments is allowed.

cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/configuration/RegistrationTest.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@
1313

1414
import com.sap.cds.feature.attachments.handler.applicationservice.CreateAttachmentsHandler;
1515
import com.sap.cds.feature.attachments.handler.applicationservice.DeleteAttachmentsHandler;
16+
import com.sap.cds.feature.attachments.handler.applicationservice.ItemsCountValidationHandler;
1617
import com.sap.cds.feature.attachments.handler.applicationservice.ReadAttachmentsHandler;
1718
import com.sap.cds.feature.attachments.handler.applicationservice.UpdateAttachmentsHandler;
1819
import com.sap.cds.feature.attachments.handler.draftservice.DraftActiveAttachmentsHandler;
1920
import com.sap.cds.feature.attachments.handler.draftservice.DraftCancelAttachmentsHandler;
2021
import com.sap.cds.feature.attachments.handler.draftservice.DraftPatchAttachmentsHandler;
22+
import com.sap.cds.feature.attachments.handler.draftservice.DraftSaveItemsCountValidationHandler;
2123
import com.sap.cds.feature.attachments.service.AttachmentService;
2224
import com.sap.cds.feature.attachments.service.handler.DefaultAttachmentsServiceHandler;
2325
import com.sap.cds.feature.attachments.service.malware.DefaultAttachmentMalwareScanner;
@@ -108,7 +110,7 @@ void handlersAreRegistered() {
108110

109111
cut.eventHandlers(configurer);
110112

111-
var handlerSize = 8;
113+
var handlerSize = 10;
112114
verify(configurer, times(handlerSize)).eventHandler(handlerArgumentCaptor.capture());
113115
checkHandlers(handlerArgumentCaptor.getAllValues(), handlerSize);
114116
}
@@ -128,7 +130,7 @@ void handlersAreRegisteredWithoutOutboxService() {
128130

129131
cut.eventHandlers(configurer);
130132

131-
var handlerSize = 8;
133+
var handlerSize = 10;
132134
verify(configurer, times(handlerSize)).eventHandler(handlerArgumentCaptor.capture());
133135
checkHandlers(handlerArgumentCaptor.getAllValues(), handlerSize);
134136
}
@@ -143,6 +145,8 @@ private void checkHandlers(List<EventHandler> handlers, int handlerSize) {
143145
isHandlerForClassIncluded(handlers, DraftPatchAttachmentsHandler.class);
144146
isHandlerForClassIncluded(handlers, DraftCancelAttachmentsHandler.class);
145147
isHandlerForClassIncluded(handlers, DraftActiveAttachmentsHandler.class);
148+
isHandlerForClassIncluded(handlers, ItemsCountValidationHandler.class);
149+
isHandlerForClassIncluded(handlers, DraftSaveItemsCountValidationHandler.class);
146150
}
147151

148152
@Test

0 commit comments

Comments
 (0)