diff --git a/README.md b/README.md index 61cb7337..7cb8be17 100644 --- a/README.md +++ b/README.md @@ -744,6 +744,26 @@ annotate Attachments with @Common: {SideEffects #ContentChanged: { - Replace `AdminService` in `Action: 'AdminService.createLink'` with the name of your service. - Repeat for other entities and elements if you have defined multiple `composition of many Attachments`. +### Updating Tenant Databases for Link Feature +To support the Link feature, additional database columns are introduced. +Upon re-deployment of your multitenant application, you may encounter "invalid column" errors if tenant database containers are not updated. + +To resolve this, ensure the following hook command is added to the mta.yaml for the sidecar application. +``` +hooks: +- name: upgrade-all +type: task +phases: +- blue-green.application.before-start.idle +- deploy.application.before-start +parameters: +name: upgrade +memory: 512M +disk-quota: 768M +command: npx -p @sap/cds-mtx cds-mtx upgrade "*" +``` +This will automatically update tenant databases during deployment. See this [example](https://github.com/vibhutikumar07/cloud-cap-samples-java/blob/31009de404af0ddc92b8c593b21395757ed053e6/mta.yaml#L71). + ## Support for edit of link type attachments This plugin provides the capability to update/edit the URL of attachments of link type. @@ -834,6 +854,7 @@ annotate Attachments with @Common: {SideEffects #ContentChanged: { - Replace `AdminService` in `Action: 'AdminService.editLink'` with the name of your service. - Repeat for other entities and elements if you have defined multiple `composition of many Attachments`. + ## Known Restrictions - UI5 Version 1.135.0: This version causes error in upload of attachments. diff --git a/sdm/src/main/java/com/sap/cds/sdm/configuration/Registration.java b/sdm/src/main/java/com/sap/cds/sdm/configuration/Registration.java index 88c0a0f5..639d6730 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/configuration/Registration.java +++ b/sdm/src/main/java/com/sap/cds/sdm/configuration/Registration.java @@ -97,7 +97,12 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) { dbQueryInstance, tokenHandlerInstance)); configurer.eventHandler( - new SDMCustomServiceHandler(sdmService, draftServiceList, tokenHandlerInstance)); + new SDMCustomServiceHandler( + sdmService, + draftServiceList, + tokenHandlerInstance, + dbQueryInstance, + persistenceService)); } private AttachmentService buildAttachmentService() { diff --git a/sdm/src/main/java/com/sap/cds/sdm/model/CmisDocument.java b/sdm/src/main/java/com/sap/cds/sdm/model/CmisDocument.java index 4af9b549..88c9d04f 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/model/CmisDocument.java +++ b/sdm/src/main/java/com/sap/cds/sdm/model/CmisDocument.java @@ -26,4 +26,5 @@ public class CmisDocument { private String subdomain; private String url; private String contentId; + private String type; } diff --git a/sdm/src/main/java/com/sap/cds/sdm/persistence/DBQuery.java b/sdm/src/main/java/com/sap/cds/sdm/persistence/DBQuery.java index 4c9b57fb..1133af36 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/persistence/DBQuery.java +++ b/sdm/src/main/java/com/sap/cds/sdm/persistence/DBQuery.java @@ -10,6 +10,7 @@ import com.sap.cds.reflect.CdsEntity; import com.sap.cds.sdm.constants.SDMConstants; import com.sap.cds.sdm.model.CmisDocument; +import com.sap.cds.sdm.service.handler.AttachmentCopyEventContext; import com.sap.cds.services.persistence.PersistenceService; import java.util.*; @@ -62,6 +63,38 @@ public CmisDocument getObjectIdForAttachmentID( return cmisDocument; } + public CmisDocument getAttachmentForObjectID( + PersistenceService persistenceService, String id, AttachmentCopyEventContext context) { + Optional attachmentEntity = context.getModel().findEntity(context.getFacet()); + CqnSelect q = + Select.from(attachmentEntity.get()) + .columns("linkUrl", "type") + .where(doc -> doc.get("objectId").eq(id)); + Result result = persistenceService.run(q); + Optional res = result.first(); + CmisDocument cmisDocument = new CmisDocument(); + if (res.isPresent()) { + Row row = res.get(); + cmisDocument.setType(row.get("type") != null ? row.get("type").toString() : null); + cmisDocument.setUrl(row.get("linkUrl") != null ? row.get("linkUrl").toString() : null); + } else { + // Check in draft table as well + attachmentEntity = context.getModel().findEntity(context.getFacet() + "_drafts"); + q = + Select.from(attachmentEntity.get()) + .columns("linkUrl", "type") + .where(doc -> doc.get("objectId").eq(id)); + result = persistenceService.run(q); + res = result.first(); + if (res.isPresent()) { + Row row = res.get(); + cmisDocument.setType(row.get("type") != null ? row.get("type").toString() : null); + cmisDocument.setUrl(row.get("linkUrl") != null ? row.get("linkUrl").toString() : null); + } + } + return cmisDocument; + } + public Result getAttachmentsForUPIDAndRepository( CdsEntity attachmentEntity, PersistenceService persistenceService, @@ -96,9 +129,7 @@ public void addAttachmentToDraft( updatedFields.put("repositoryId", repositoryId); updatedFields.put("folderId", cmisDocument.getFolderId()); updatedFields.put("status", "Clean"); - String icon = getIconForMimeType(cmisDocument.getMimeType()); - updatedFields.put("type", icon); - + updatedFields.put("type", "sap-icon://document"); CqnUpdate updateQuery = Update.entity(attachmentEntity) .data(updatedFields) @@ -106,66 +137,6 @@ public void addAttachmentToDraft( persistenceService.run(updateQuery); } - private static String getIconForMimeType(String mimeType) { - if (isExcel(mimeType)) { - return "sap-icon://excel-attachment"; - } else if (isImage(mimeType)) { - return "sap-icon://attachment-photo"; - } else if (isText(mimeType)) { - return "sap-icon://attachment-text-file"; - } else if (isPdf(mimeType)) { - return "sap-icon://pdf-attachment"; - } else if (isPowerPoint(mimeType)) { - return "sap-icon://ppt-attachment"; - } else if (isVideo(mimeType)) { - return "sap-icon://attachment-video"; - } else if (isAudio(mimeType)) { - return "sap-icon://attachment-audio"; - } else if (isZip(mimeType)) { - return "sap-icon://attachment-zip-file"; - } else if (isHtml(mimeType)) { - return "sap-icon://attachment-html"; - } - return "sap-icon://document"; - } - - private static boolean isExcel(String mimeType) { - return mimeType.contains("vnd.ms-excel") - || mimeType.contains("vnd.openxmlformats-officedocument.spreadsheetml.sheet"); - } - - private static boolean isImage(String mimeType) { - return mimeType.contains("image"); - } - - private static boolean isText(String mimeType) { - return mimeType.contains("text"); - } - - private static boolean isPdf(String mimeType) { - return mimeType.contains("pdf"); - } - - private static boolean isPowerPoint(String mimeType) { - return mimeType.contains("powerpoint") || mimeType.contains("presentation"); - } - - private static boolean isVideo(String mimeType) { - return mimeType.contains("video"); - } - - private static boolean isAudio(String mimeType) { - return mimeType.contains("audio"); - } - - private static boolean isZip(String mimeType) { - return mimeType.contains("zip"); - } - - private static boolean isHtml(String mimeType) { - return mimeType.contains("html"); - } - public List getAttachmentsForFolder( String entity, PersistenceService persistenceService, diff --git a/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMCustomServiceHandler.java b/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMCustomServiceHandler.java index 1daf78b1..b87e0bb3 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMCustomServiceHandler.java +++ b/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMCustomServiceHandler.java @@ -9,12 +9,14 @@ import com.sap.cds.sdm.handler.TokenHandler; import com.sap.cds.sdm.model.CmisDocument; import com.sap.cds.sdm.model.SDMCredentials; +import com.sap.cds.sdm.persistence.DBQuery; import com.sap.cds.sdm.service.RegisterService; import com.sap.cds.sdm.service.SDMService; import com.sap.cds.services.ServiceException; import com.sap.cds.services.draft.DraftService; import com.sap.cds.services.handler.annotations.On; import com.sap.cds.services.handler.annotations.ServiceName; +import com.sap.cds.services.persistence.PersistenceService; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; @@ -26,14 +28,22 @@ @ServiceName(value = "*", type = RegisterService.class) public class SDMCustomServiceHandler { private final SDMService sdmService; + private final DBQuery dbQuery; private final List draftService; private final TokenHandler tokenHandler; + private final PersistenceService persistenceService; public SDMCustomServiceHandler( - SDMService sdmService, List draftService, TokenHandler tokenHandler) { + SDMService sdmService, + List draftService, + TokenHandler tokenHandler, + DBQuery dbQuery, + PersistenceService persistenceService) { this.sdmService = sdmService; this.draftService = draftService; this.tokenHandler = tokenHandler; + this.dbQuery = dbQuery; + this.persistenceService = persistenceService; } @On(event = RegisterService.EVENT_COPY_ATTACHMENT) @@ -62,12 +72,15 @@ public void copyAttachments(AttachmentCopyEventContext context) throws IOExcepti folderId = succinctProperties.getString("cmis:objectId"); } CmisDocument cmisDocument = new CmisDocument(); - cmisDocument.setRepositoryId(repositoryId); - cmisDocument.setFolderId(folderId); + List objectIds = context.getObjectIds(); List> attachmentsMetadata = new ArrayList<>(); for (String objectId : objectIds) { + // get Link Url from objectId and set to cmisDocument + cmisDocument = dbQuery.getAttachmentForObjectID(persistenceService, objectId, context); cmisDocument.setObjectId(objectId); + cmisDocument.setRepositoryId(repositoryId); + cmisDocument.setFolderId(folderId); try { attachmentsMetadata.add( sdmService.copyAttachment(cmisDocument, sdmCredentials, isSystemUser)); @@ -101,15 +114,21 @@ public void copyAttachments(AttachmentCopyEventContext context) throws IOExcepti for (List attachmentMetadata : attachmentsMetadata) { String fileName = attachmentMetadata.get(0); String mimeType = attachmentMetadata.get(1); + if (mimeType.equalsIgnoreCase("application/internet-shortcut")) { + int dotIndex = fileName.lastIndexOf('.'); + fileName = fileName.substring(0, dotIndex); + } String newObjectId = attachmentMetadata.get(2); updatedFields.put("objectId", newObjectId); updatedFields.put("repositoryId", repositoryId); updatedFields.put("folderId", folderId); updatedFields.put("status", "Clean"); updatedFields.put("mimeType", mimeType); + updatedFields.put("type", cmisDocument.getType()); updatedFields.put("fileName", fileName); updatedFields.put("HasDraftEntity", false); updatedFields.put("HasActiveEntity", false); + updatedFields.put("linkUrl", cmisDocument.getUrl()); updatedFields.put( "contentId", newObjectId + ":" + folderId + ":" + context.getFacet() + ":" + mimeType); updatedFields.put(upIdKey, upID); diff --git a/sdm/src/main/resources/cds/com.sap.cds/sdm/attachments.cds b/sdm/src/main/resources/cds/com.sap.cds/sdm/attachments.cds index 10b21f05..e26b9bb6 100644 --- a/sdm/src/main/resources/cds/com.sap.cds/sdm/attachments.cds +++ b/sdm/src/main/resources/cds/com.sap.cds/sdm/attachments.cds @@ -6,7 +6,7 @@ extend aspect Attachments with { repositoryId : String; objectId : String; linkUrl : String default null; - type : String @(UI: {IsImageURL: true}) default null; + type : String @(UI: {IsImageURL: true}) default 'sap-icon://document'; } annotate Attachments with @UI: { HeaderInfo: { diff --git a/sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMCustomServiceHandlerTest.java b/sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMCustomServiceHandlerTest.java index 8cf28173..851c412f 100644 --- a/sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMCustomServiceHandlerTest.java +++ b/sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMCustomServiceHandlerTest.java @@ -12,12 +12,15 @@ import com.sap.cds.reflect.CdsEntity; import com.sap.cds.reflect.CdsModel; import com.sap.cds.sdm.handler.TokenHandler; +import com.sap.cds.sdm.model.CmisDocument; import com.sap.cds.sdm.model.SDMCredentials; +import com.sap.cds.sdm.persistence.DBQuery; import com.sap.cds.sdm.service.SDMService; import com.sap.cds.sdm.service.handler.AttachmentCopyEventContext; import com.sap.cds.sdm.service.handler.SDMCustomServiceHandler; import com.sap.cds.services.ServiceException; import com.sap.cds.services.draft.DraftService; +import com.sap.cds.services.persistence.PersistenceService; import com.sap.cds.services.request.UserInfo; import java.io.IOException; import java.util.List; @@ -33,11 +36,14 @@ public class SDMCustomServiceHandlerTest { @Mock private AttachmentCopyEventContext mockContext; @Mock private SDMService sdmService; + @Mock private PersistenceService persistenceService; @Mock private TokenHandler tokenHandler; @Mock private DraftService draftService; + @Mock private DBQuery dbQuery; + private SDMCustomServiceHandler sdmCustomServiceHandler; private static final String OBJECT_ID = "mockObjectId"; @@ -51,7 +57,8 @@ void setUp() { when(draftService.getName()).thenReturn(FACET); // Pass a non-null list of DraftService mocks sdmCustomServiceHandler = - new SDMCustomServiceHandler(sdmService, List.of(draftService), tokenHandler); + new SDMCustomServiceHandler( + sdmService, List.of(draftService), tokenHandler, dbQuery, persistenceService); } @Test @@ -60,6 +67,39 @@ void testCopyAttachments_HappyPath() throws IOException { SDMCredentials sdmCredentials = mock(SDMCredentials.class); when(tokenHandler.getSDMCredentials()).thenReturn(sdmCredentials); + // Mock folder id retrieval + when(sdmService.getFolderIdByPath( + any(String.class), any(String.class), any(SDMCredentials.class), any(Boolean.class))) + .thenReturn(FOLDER_ID); + + // Mock attachment copy + when(sdmService.copyAttachment(any(), any(SDMCredentials.class), any(Boolean.class))) + .thenReturn(List.of("fileName.url", "application/internet-shortcut", OBJECT_ID)); + CmisDocument cmisDocument = new CmisDocument(); + cmisDocument.setType("sap-icon://internet-browser"); + cmisDocument.setUrl("https://example.com"); + when(dbQuery.getAttachmentForObjectID(any(), any(), any())).thenReturn(cmisDocument); + + // Mock context + AttachmentCopyEventContext context = createMockContext(); + context.setObjectIds(List.of(OBJECT_ID)); + + // Act + sdmCustomServiceHandler.copyAttachments(context); + + // Assert + verify(sdmService, times(1)) + .copyAttachment(any(), any(SDMCredentials.class), any(Boolean.class)); + verify(draftService, times(1)).newDraft(any()); + verify(context, times(1)).setCompleted(); + } + + @Test + void testCopyAttachments_HappyPathNonLink() throws IOException { + // Mock SDMCredentials + SDMCredentials sdmCredentials = mock(SDMCredentials.class); + when(tokenHandler.getSDMCredentials()).thenReturn(sdmCredentials); + // Mock folder id retrieval when(sdmService.getFolderIdByPath( any(String.class), any(String.class), any(SDMCredentials.class), any(Boolean.class))) @@ -68,6 +108,9 @@ void testCopyAttachments_HappyPath() throws IOException { // Mock attachment copy when(sdmService.copyAttachment(any(), any(SDMCredentials.class), any(Boolean.class))) .thenReturn(List.of("fileName", "mimeType", OBJECT_ID)); + CmisDocument cmisDocument = new CmisDocument(); + cmisDocument.setType("sap-icon://document"); + when(dbQuery.getAttachmentForObjectID(any(), any(), any())).thenReturn(cmisDocument); // Mock context AttachmentCopyEventContext context = createMockContext(); @@ -117,6 +160,10 @@ void testCopyAttachments_FolderDoesNotExist() throws IOException { // Mock attachment copy when(sdmService.copyAttachment(any(), any(SDMCredentials.class), any(Boolean.class))) .thenReturn(List.of("fileName", "mimeType", OBJECT_ID)); + CmisDocument cmisDocument = new CmisDocument(); + cmisDocument.setType("sap-icon://internet-browser"); + cmisDocument.setUrl("https://example.com"); + when(dbQuery.getAttachmentForObjectID(any(), any(), any())).thenReturn(cmisDocument); // Mock context AttachmentCopyEventContext context = createMockContext(); @@ -149,6 +196,10 @@ void testCopyAttachments_AttachmentCopyFails() throws IOException { when(sdmService.copyAttachment(any(), any(SDMCredentials.class), any(Boolean.class))) .thenReturn(List.of("fileName", "mimeType", OBJECT_ID)) .thenThrow(new ServiceException("Copy failed")); + CmisDocument cmisDocument = new CmisDocument(); + cmisDocument.setType("sap-icon://internet-browser"); + cmisDocument.setUrl("https://example.com"); + when(dbQuery.getAttachmentForObjectID(any(), any(), any())).thenReturn(cmisDocument); // Mock context AttachmentCopyEventContext context = createMockContext(); @@ -181,6 +232,11 @@ void testCopyAttachments_AttachmentCopyFails_FolderDoesNotExist() throws IOExcep UserInfo userInfo = mock(UserInfo.class); when(context.getUserInfo()).thenReturn(userInfo); when(userInfo.getName()).thenReturn("testUser"); + CmisDocument cmisDocument = new CmisDocument(); + cmisDocument.setType("sap-icon://internet-browser"); + cmisDocument.setUrl("https://example.com"); + when(dbQuery.getAttachmentForObjectID(any(), any(), any())).thenReturn(cmisDocument); + ServiceException ex = assertThrows( ServiceException.class, @@ -212,6 +268,11 @@ void testCopyAttachments_AttachmentCopyFails_FolderExists_AttachmentsDeleted() UserInfo userInfo = mock(UserInfo.class); when(context.getUserInfo()).thenReturn(userInfo); when(userInfo.getName()).thenReturn("testUser"); + CmisDocument cmisDocument = new CmisDocument(); + cmisDocument.setType("sap-icon://internet-browser"); + cmisDocument.setUrl("https://example.com"); + when(dbQuery.getAttachmentForObjectID(any(), any(), any())).thenReturn(cmisDocument); + ServiceException ex = assertThrows( ServiceException.class,