diff --git a/.github/actions/newrelease/action.yml b/.github/actions/newrelease/action.yml index 04296d557..965980797 100644 --- a/.github/actions/newrelease/action.yml +++ b/.github/actions/newrelease/action.yml @@ -34,8 +34,8 @@ runs: #./ensure-license.sh git config --global user.name 'github-actions[bot]' git config --global user.email 'github-actions[bot]@users.noreply.github.com' - git checkout -b develop + git checkout -b Release_v1.10.2 git add cap-notebook/version.txt git commit -am "Update version to $VERSION" - git push --set-upstream origin develop + git push --set-upstream origin Release_v1.10.2 shell: bash diff --git a/.github/workflows/main-build-and-deploy-oss.yml b/.github/workflows/main-build-and-deploy-oss.yml index 74bfb7326..c99c8f129 100644 --- a/.github/workflows/main-build-and-deploy-oss.yml +++ b/.github/workflows/main-build-and-deploy-oss.yml @@ -9,7 +9,7 @@ on: types: [ "released" ] permissions: - contents: read + contents: write jobs: diff --git a/cap-notebook/version.txt b/cap-notebook/version.txt new file mode 100644 index 000000000..5ad2491cf --- /dev/null +++ b/cap-notebook/version.txt @@ -0,0 +1 @@ +1.10.2 diff --git a/pom.xml b/pom.xml index c188cfe7d..ae1735a15 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ - 1.10.1-SNAPSHOT + 1.10.3.2-SNAPSHOT 21 ${java.version} ${java.version} diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/TokenHandler.java b/sdm/src/main/java/com/sap/cds/sdm/handler/TokenHandler.java index ced6701f9..f2919d3fe 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/handler/TokenHandler.java +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/TokenHandler.java @@ -56,6 +56,7 @@ public String toString(byte[] bytes) { } public SDMCredentials getSDMCredentials() { + logger.debug("START: getSDMCredentials - loading SDM credentials from service binding"); Map uaaCredentials = getUaaCredentials(); Map uaa = (Map) uaaCredentials.get("uaa"); SDMCredentials sdmCredentials = new SDMCredentials(); @@ -63,17 +64,27 @@ public SDMCredentials getSDMCredentials() { sdmCredentials.setUrl(uaaCredentials.get("uri").toString()); sdmCredentials.setClientId(uaa.get("clientid").toString()); sdmCredentials.setClientSecret(uaa.get("clientsecret").toString()); + logger.debug("END: getSDMCredentials - SDM URL: {}", sdmCredentials.getUrl()); return sdmCredentials; } public Map getUaaCredentials() { + logger.debug("START: getUaaCredentials - scanning service bindings for 'sdm' tag"); List allServiceBindings = DefaultServiceBindingAccessor.getInstance().getServiceBindings(); + logger.debug("Total service bindings found: {}", allServiceBindings.size()); ServiceBinding sdmBinding = allServiceBindings.stream() .filter(binding -> binding.getTags().contains("sdm")) .findFirst() - .orElseThrow(() -> new IllegalStateException("SDM binding not found")); + .orElseThrow( + () -> { + logger.error( + "No service binding with 'sdm' tag found among {} bindings", + allServiceBindings.size()); + return new IllegalStateException("SDM binding not found"); + }); + logger.debug("END: getUaaCredentials - SDM binding found"); return sdmBinding.getCredentials(); } @@ -83,10 +94,19 @@ public HttpClient getHttpClient( String subdomain, String type) { + logger.debug( + "START: getHttpClient - type: {}, subdomain: {}, connectionPoolConfig: {}", + type, + subdomain, + connectionPoolConfig != null ? "configured" : "null(using defaults)"); + Map uaaCredentials; if (binding != null && !binding.getCredentials().isEmpty()) { + logger.debug("getHttpClient - using credentials from provided ServiceBinding"); uaaCredentials = binding.getCredentials(); } else { + logger.debug( + "getHttpClient - binding not provided or empty, fetching from service binding registry"); uaaCredentials = getUaaCredentials(); } @@ -94,26 +114,35 @@ public HttpClient getHttpClient( ClientCredentials clientCredentials = new ClientCredentials(uaa.get(CLIENT_ID).toString(), uaa.get(CLIENT_SECRET).toString()); + logger.debug("getHttpClient - clientId: {}", uaa.get(CLIENT_ID).toString()); String baseTokenUrl = uaa.get(SDM_TOKEN_ENDPOINT).toString(); + logger.debug("getHttpClient - base token URL: {}", baseTokenUrl); if (subdomain != null && !subdomain.isEmpty()) { String providerSubdomain = baseTokenUrl.substring(baseTokenUrl.indexOf("/") + 2, baseTokenUrl.indexOf(".")); baseTokenUrl = baseTokenUrl.replace(providerSubdomain, subdomain); + logger.debug( + "getHttpClient - token URL adjusted for subdomain '{}': {}", subdomain, baseTokenUrl); } + String sdmTargetUrl = uaaCredentials.get(SDM_URL).toString(); + logger.debug("getHttpClient - SDM target URL: {}", sdmTargetUrl); + DefaultHttpDestination destination; if (NAMED_USER_FLOW.equals(type)) { + logger.debug("getHttpClient - building NAMED_USER (token exchange) destination"); destination = - OAuth2DestinationBuilder.forTargetUrl(uaaCredentials.get(SDM_URL).toString()) + OAuth2DestinationBuilder.forTargetUrl(sdmTargetUrl) .withTokenEndpoint(baseTokenUrl) .withClient(clientCredentials, OnBehalfOf.NAMED_USER_CURRENT_TENANT) .property( SDMConstants.SDM_DESTINATION_KEY, SDMConstants.SDM_TOKEN_EXCHANGE_DESTINATION) .build(); } else { + logger.debug("getHttpClient - building TECHNICAL_USER (client credentials) destination"); destination = - OAuth2DestinationBuilder.forTargetUrl(uaaCredentials.get(SDM_URL).toString()) + OAuth2DestinationBuilder.forTargetUrl(sdmTargetUrl) .withTokenEndpoint(baseTokenUrl) .withClient(clientCredentials, OnBehalfOf.TECHNICAL_USER_CURRENT_TENANT) .property( @@ -130,20 +159,36 @@ public HttpClient getHttpClient( builder.timeoutMilliseconds((int) timeout.toMillis()); builder.maxConnectionsPerRoute(SDMConstants.MAX_CONNECTIONS); builder.maxConnectionsTotal(SDMConstants.MAX_CONNECTIONS); + logger.debug( + "getHttpClient - using default connection pool: timeout={}ms, maxConnPerRoute={}," + + " maxConnTotal={}", + timeout.toMillis(), + SDMConstants.MAX_CONNECTIONS, + SDMConstants.MAX_CONNECTIONS); } else { builder.timeoutMilliseconds((int) connectionPoolConfig.getTimeout().toMillis()); builder.maxConnectionsPerRoute(connectionPoolConfig.getMaxConnectionsPerRoute()); builder.maxConnectionsTotal(connectionPoolConfig.getMaxConnections()); + logger.debug( + "getHttpClient - using configured connection pool: timeout={}ms, maxConnPerRoute={}," + + " maxConnTotal={}", + connectionPoolConfig.getTimeout().toMillis(), + connectionPoolConfig.getMaxConnectionsPerRoute(), + connectionPoolConfig.getMaxConnections()); } - return builder.build().createHttpClient(destination); + HttpClient httpClient = builder.build().createHttpClient(destination); + logger.debug("END: getHttpClient - HttpClient created for type: {}", type); + return httpClient; } public HttpClient getHttpClientForAuthoritiesFlow( CdsProperties.ConnectionPool connectionPoolConfig, String user) { + logger.debug("START: getHttpClientForAuthoritiesFlow - user: {}", user); Optional destinations = getHttpDestination(user); if (destinations.isPresent()) { + logger.debug("getHttpClientForAuthoritiesFlow - HttpDestination resolved for user: {}", user); DefaultHttpClientFactory.DefaultHttpClientFactoryBuilder builder = DefaultHttpClientFactory.builder(); @@ -152,25 +197,40 @@ public HttpClient getHttpClientForAuthoritiesFlow( builder.timeoutMilliseconds((int) timeout.toMillis()); builder.maxConnectionsPerRoute(SDMConstants.MAX_CONNECTIONS); builder.maxConnectionsTotal(SDMConstants.MAX_CONNECTIONS); + logger.debug( + "getHttpClientForAuthoritiesFlow - using default pool: timeout={}ms", + timeout.toMillis()); } else { builder.timeoutMilliseconds((int) connectionPoolConfig.getTimeout().toMillis()); builder.maxConnectionsPerRoute(connectionPoolConfig.getMaxConnectionsPerRoute()); builder.maxConnectionsTotal(connectionPoolConfig.getMaxConnections()); + logger.debug( + "getHttpClientForAuthoritiesFlow - using configured pool: timeout={}ms", + connectionPoolConfig.getTimeout().toMillis()); } - return builder.build().createHttpClient(destinations.get()); + HttpClient httpClient = builder.build().createHttpClient(destinations.get()); + logger.debug("END: getHttpClientForAuthoritiesFlow - HttpClient created for user: {}", user); + return httpClient; } + logger.warn( + "getHttpClientForAuthoritiesFlow - no HttpDestination found for user: {}," + + " returning null", + user); return null; } private Optional getHttpDestination(String userName) { + logger.debug("START: getHttpDestination - resolving destination for user: {}", userName); HttpDestination httpDestination; try { httpDestination = ServiceBindingDestinationLoader.defaultLoaderChain() .getDestination(getSDMDestinationOptions(userName)); + logger.debug("END: getHttpDestination - destination resolved for user: {}", userName); } catch (Exception exception) { - logger.error("Error with fetching httpdestination " + exception.getCause()); + logger.error( + "Error with fetching httpdestination for user {}: {}", userName, exception.getCause()); httpDestination = null; } return Optional.ofNullable(httpDestination); diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMReadAttachmentsHandler.java b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMReadAttachmentsHandler.java index 241e28531..5a77a7ece 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMReadAttachmentsHandler.java +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMReadAttachmentsHandler.java @@ -133,7 +133,7 @@ public void processBefore(CdsReadEventContext context) throws IOException { return; } setErrorMessagesInCache(context); - if (context.getTarget().getAnnotationValue(SDMConstants.ANNOTATION_IS_MEDIA_DATA, false)) { + if (SDMApplicationHandlerHelper.isMediaEntity(context.getTarget())) { try { // update the uploadStatus of all blank attachments with success this is for existing // attachments @@ -147,6 +147,21 @@ public void processBefore(CdsReadEventContext context) throws IOException { repoValue.getIsAsyncVirusScanEnabled()); Optional attachmentDraftEntity = context.getModel().findEntity(context.getTarget().getQualifiedName() + "_drafts"); + logger.debug( + "Draft entity: {}", + attachmentDraftEntity.isPresent() + ? attachmentDraftEntity.get().getQualifiedName() + : "No draft entity"); + Optional attachmentActiveEntity = + context.getModel().findEntity(context.getTarget().getQualifiedName()); + logger.debug( + "Active entity: {}", + attachmentActiveEntity.isPresent() + ? attachmentActiveEntity.get().getQualifiedName() + : "No active entity"); + if (attachmentDraftEntity.isEmpty() && attachmentActiveEntity.isPresent()) { + attachmentDraftEntity = attachmentActiveEntity; + } String upIdKey = "", upID = ""; if (attachmentDraftEntity.isPresent()) { upIdKey = SDMUtils.getUpIdKey(attachmentDraftEntity.get()); diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/AttachmentsHandlerUtils.java b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/AttachmentsHandlerUtils.java index f5bc8ee93..b9f71050e 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/AttachmentsHandlerUtils.java +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/AttachmentsHandlerUtils.java @@ -194,7 +194,7 @@ private static void processAttachmentPaths( List attachmentPaths) { for (String attachmentPath : attachmentPaths) { String entityPath = buildEntityPath(entity, targetEntity, attachmentPath); - String actualPath = buildActualPath(entity, compositionName, attachmentPath); + String actualPath = buildActualPath(entity, compositionName, targetEntity, attachmentPath); // Only add the mapping if both paths are non-null and the key doesn't already exist // This preserves direct attachment mappings from being overwritten by nested ones @@ -272,8 +272,9 @@ private static String buildEntityPath( // Direct attachment: use parent entity path entityPath = parentEntity.getQualifiedName() + "." + attachmentPart; } else { - // Nested attachment: use target entity path to ensure uniqueness - entityPath = targetEntity.getQualifiedName() + "." + attachmentPart; + // Nested attachment: use attachmentPath as-is — it already encodes the owning entity + // (e.g. "AdminService.Sections.attachments"), ensuring uniqueness at any depth. + entityPath = attachmentPath; } return entityPath; } @@ -284,19 +285,34 @@ private static String buildEntityPath( } private static String buildActualPath( - CdsEntity parentEntity, String compositionPropertyName, String attachmentPath) { + CdsEntity parentEntity, + String compositionPropertyName, + CdsEntity targetEntity, + String attachmentPath) { try { String[] pathParts = attachmentPath.split("\\."); if (pathParts.length >= 3) { - // Get the attachment part (last part) String attachmentPart = pathParts[pathParts.length - 1]; - - // Build the new path using parent entity qualified name + composition property name - return parentEntity.getQualifiedName() - + "." - + compositionPropertyName - + "." - + attachmentPart; + String ownerEntityQN = attachmentPath.substring(0, attachmentPath.lastIndexOf('.')); + + if (ownerEntityQN.equals(targetEntity.getQualifiedName())) { + return parentEntity.getQualifiedName() + + "." + + compositionPropertyName + + "." + + attachmentPart; + } else { + String intermediatePath = findPathToEntity(targetEntity, ownerEntityQN, new HashSet<>()); + if (intermediatePath != null) { + return parentEntity.getQualifiedName() + + "." + + compositionPropertyName + + "." + + intermediatePath + + "." + + attachmentPart; + } + } } } catch (Exception e) { logger.warn(SDMUtils.getErrorMessage("FETCH_ATTACHMENT_COMPOSITION_ERROR"), e.getMessage()); @@ -304,6 +320,27 @@ private static String buildActualPath( return null; } + private static String findPathToEntity( + CdsEntity fromEntity, String toEntityQN, Set visited) { + if (visited.contains(fromEntity.getQualifiedName())) return null; + visited.add(fromEntity.getQualifiedName()); + List comps = fromEntity.compositions().collect(java.util.stream.Collectors.toList()); + for (Object comp : comps) { + com.sap.cds.reflect.CdsElement element = (com.sap.cds.reflect.CdsElement) comp; + if (!element.getType().isAssociation()) continue; + CdsAssociationType assocType = (CdsAssociationType) element.getType(); + CdsEntity target = assocType.getTarget(); + if (toEntityQN.equals(target.getQualifiedName())) { + return element.getName(); + } + String subPath = findPathToEntity(target, toEntityQN, visited); + if (subPath != null) { + return element.getName() + "." + subPath; + } + } + return null; + } + private static List> findNestedAttachments( Map entity, String attachmentKey, String parentKey, String currentParentKey) { List> result = new ArrayList<>(); diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMApplicationHandlerHelper.java b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMApplicationHandlerHelper.java index 5d8d692d3..a4b251567 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMApplicationHandlerHelper.java +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/common/SDMApplicationHandlerHelper.java @@ -13,14 +13,31 @@ public final class SDMApplicationHandlerHelper { private static final String ANNOTATION_IS_MEDIA_DATA = "_is_media_data"; /** - * Checks if the entity is a media entity. A media entity is an entity that is annotated with the - * annotation "_is_media_data". + * Checks if the entity is a media entity. A media entity is one that carries the "_is_media_data" + * annotation (set by the CAP attachments plugin on DB-backed attachment entities), or — as a + * fallback for service-layer-only draft entities whose annotation may not be propagated — one + * that has both the SDM-specific "objectId" element and the "content" element that is + * characteristic of the sap.attachments.Attachments aspect. * * @param baseEntity The entity to check * @return true if the entity is a media entity, false otherwise */ public static boolean isMediaEntity(CdsStructuredType baseEntity) { boolean isMedia = baseEntity.getAnnotationValue(ANNOTATION_IS_MEDIA_DATA, false); + if (!isMedia) { + // Fallback for service-layer-only entities (e.g. SupplierBidTermValuesServiceEntity + // attachments) whose inline Composition of Attachments does not receive the + // _is_media_data annotation at runtime. Presence of both "objectId" (SDM-specific + // extension) and "content" (core MediaData field) is a reliable structural signal. + isMedia = + baseEntity.findElement("objectId").isPresent() + && baseEntity.findElement("content").isPresent(); + if (isMedia) { + logger.debug( + "Entity {} identified as media entity via structural fallback (objectId + content)", + baseEntity.getQualifiedName()); + } + } logger.debug("Entity {} isMediaEntity: {}", baseEntity.getQualifiedName(), isMedia); return isMedia; } 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 33f5dc9e4..219c4f2e9 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 @@ -558,6 +558,11 @@ public Map getPropertiesForID( PersistenceService persistenceService, String id, Map properties) { + logger.debug( + "getPropertiesForID - entity: {}, id: {}, fetching properties: {}", + attachmentEntity.getQualifiedName(), + id, + properties.keySet()); CqnSelect q = Select.from(attachmentEntity) .columns(properties.keySet().toArray(new String[0])) @@ -571,6 +576,7 @@ public Map getPropertiesForID( Object value = result.rowCount() > 0 ? result.list().get(0).get(property) : null; propertyValueMap.put(mapKey, value != null ? value.toString() : null); } + logger.debug("getPropertiesForID - id: {}, result map: {}", id, propertyValueMap); return propertyValueMap; } diff --git a/sdm/src/main/java/com/sap/cds/sdm/service/ReadAheadInputStream.java b/sdm/src/main/java/com/sap/cds/sdm/service/ReadAheadInputStream.java index 0ef108b33..b9fcf1b13 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/service/ReadAheadInputStream.java +++ b/sdm/src/main/java/com/sap/cds/sdm/service/ReadAheadInputStream.java @@ -49,6 +49,7 @@ private void preloadChunks() { executor.submit( () -> { try { + int chunkCount = 0; while (totalBytesRead.get() < totalSize) { AtomicReference bufferRef = new AtomicReference<>(new byte[CHUNK_SIZE]); AtomicLong bytesReadAtomic = new AtomicLong(0); @@ -58,21 +59,40 @@ private void preloadChunks() { long bytesRead = bytesReadAtomic.get(); if (bytesRead > 0) { totalBytesRead.addAndGet(bytesRead); + chunkCount++; // Trim buffer if last chunk is smaller if (bytesRead < CHUNK_SIZE) { byte[] trimmedBuffer = new byte[(int) bytesRead]; System.arraycopy(bufferRef.get(), 0, trimmedBuffer, 0, (int) bytesRead); bufferRef.set(trimmedBuffer); + logger.debug( + "preloadChunks - chunk {} trimmed to {} bytes (last chunk)", + chunkCount, + bytesRead); + } else { + logger.debug( + "preloadChunks - chunk {} loaded: {} bytes, totalBytesRead: {}", + chunkCount, + bytesRead, + totalBytesRead.get()); } // Ensure last chunk is enqueued chunkQueue.put(bufferRef.get()); + logger.debug( + "preloadChunks - chunk {} enqueued, queueSize: {}", + chunkCount, + chunkQueue.size()); // Only mark as last chunk after enqueuing the last chunk if (totalBytesRead.get() >= totalSize) { lastChunkLoaded.set(true); - logger.info("Last chunk successfully queued and marked."); + logger.info( + "Last chunk successfully queued and marked. Total chunks: {}," + + " totalBytesRead: {}", + chunkCount, + totalBytesRead.get()); break; } } else { @@ -203,7 +223,7 @@ private synchronized void loadNextChunk() throws IOException { @Override public synchronized int read() throws IOException { - logger.info( + logger.debug( "ReadAheadInputStream.read() called by " + Thread.currentThread().getStackTrace()[2]); if (position.get() >= currentBufferSize) { if (lastChunkLoaded.get()) return -1; // EOF diff --git a/sdm/src/main/java/com/sap/cds/sdm/service/SDMServiceImpl.java b/sdm/src/main/java/com/sap/cds/sdm/service/SDMServiceImpl.java index 15699f182..0d9b162bb 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/service/SDMServiceImpl.java +++ b/sdm/src/main/java/com/sap/cds/sdm/service/SDMServiceImpl.java @@ -132,9 +132,19 @@ private void executeHttpPost( CmisDocument cmisDocument, Map finalResponse) throws ServiceException { + logger.debug( + "executeHttpPost - POST URL: {}, file: {}", + uploadFile.getURI(), + cmisDocument.getFileName()); try (var response = (CloseableHttpResponse) httpClient.execute(uploadFile)) { + logger.debug( + "executeHttpPost - response status: {}", response.getStatusLine().getStatusCode()); formResponse(cmisDocument, finalResponse, response); } catch (IOException e) { + logger.error( + "executeHttpPost - IOException for file {}: {}", + cmisDocument.getFileName(), + e.getMessage()); throw new ServiceException( SDMUtils.getErrorMessage("ERROR_IN_SETTING_TIMEOUT"), e.getMessage()); } @@ -152,11 +162,14 @@ private void formResponse( try { String responseString = EntityUtils.toString(response.getEntity()); int responseCode = response.getStatusLine().getStatusCode(); + logger.debug("formResponse - responseCode: {}, file: {}", responseCode, name); + logger.debug("formResponse - raw response body: {}", responseString); if (responseCode == 201 || responseCode == 200) { status = "success"; JSONObject jsonResponse = new JSONObject(responseString); JSONObject succinctProperties = jsonResponse.getJSONObject("succinctProperties"); objectId = succinctProperties.getString("cmis:objectId"); + logger.debug("formResponse - upload success, objectId: {}", objectId); } else { if (responseCode == 409) { JSONObject jsonResponse = new JSONObject(responseString); @@ -165,17 +178,22 @@ private void formResponse( objectId = succinctProperties.getString("cmis:objectId"); if ("Malware Service Exception: Virus found in the file!".equals(message)) { status = "virus"; + logger.debug("formResponse - virus detected for file: {}", name); } else { status = "duplicate"; + logger.debug("formResponse - duplicate file detected: {}", name); } } else if ((responseCode == 403) && (responseString.equals("User does not have required scope"))) { status = "unauthorized"; + logger.warn("formResponse - unauthorized (403) for file: {}", name); } else { JSONObject jsonResponse = new JSONObject(responseString); String message = jsonResponse.getString("message"); status = "fail"; error = message; + logger.warn( + "formResponse - upload failed, responseCode: {}, message: {}", responseCode, message); } } // Construct the final response @@ -186,6 +204,7 @@ private void formResponse( if (!objectId.isEmpty()) { finalResponse.put("objectId", objectId); } + logger.debug("formResponse - END status: {}, objectId: {}", status, objectId); } catch (Exception e) { throw new ServiceException(e.getMessage()); } @@ -312,6 +331,11 @@ public int updateAttachments( return 200; // No updates needed, return success } + logger.debug( + "updateAttachments - PATCH URL: {}, request body keys: {}", + sdmUrl, + updateRequestBody.keySet()); + MultipartEntityBuilder builder = MultipartEntityBuilder.create(); SDMUtils.assembleRequestBodySecondaryTypes( builder, updateRequestBody, objectId); // Adding Secondary Properties to the request body @@ -320,14 +344,17 @@ public int updateAttachments( updateRequest.setEntity(builder.build()); try (var response = (CloseableHttpResponse) httpClient.execute(updateRequest)) { - if (response.getStatusLine().getStatusCode() == 400) { + int statusCode = response.getStatusLine().getStatusCode(); + logger.debug("updateAttachments - response status: {}", statusCode); + if (statusCode == 400) { String responseString = EntityUtils.toString(response.getEntity()); + logger.warn("updateAttachments - 400 Bad Request body: {}", responseString); JSONObject jsonResponse = new JSONObject(responseString); String message = jsonResponse.getString("message"); throw new ServiceException(message); } - logger.debug("END: updateAttachments - status: {}", response.getStatusLine().getStatusCode()); - return response.getStatusLine().getStatusCode(); + logger.debug("END: updateAttachments - status: {}", statusCode); + return statusCode; } catch (IOException e) { logger.error("Error updating attachments: {}", e.getMessage(), e); throw new ServiceException(SDMUtils.getErrorMessage("COULD_NOT_UPDATE_THE_ATTACHMENT"), e); @@ -537,8 +564,10 @@ public String getFolderIdByPath( + parentId + "?cmisselector=object"; HttpGet getFolderRequest = new HttpGet(sdmUrl); + logger.debug("getFolderIdByPath - GET URL: {}", sdmUrl); try (var response = (CloseableHttpResponse) httpClient.execute(getFolderRequest)) { int responseCode = response.getStatusLine().getStatusCode(); + logger.debug("getFolderIdByPath - response code: {}", responseCode); if (responseCode == 200) { JSONObject jsonObject = new JSONObject(EntityUtils.toString(response.getEntity())); folderId = @@ -546,6 +575,8 @@ public String getFolderIdByPath( .getJSONObject("properties") .getJSONObject("cmis:objectId") .getString("value"); + } else if (responseCode == 404) { + logger.debug("getFolderIdByPath - folder not found for path: {}", parentId); } else if (responseCode == 403) { throw new ServiceException(SDMUtils.getErrorMessage("USER_NOT_AUTHORISED_ERROR")); } @@ -575,15 +606,18 @@ public String createFolder( builder.addTextBody("succinct", "true", ContentType.TEXT_PLAIN); HttpEntity multipart = builder.build(); createFolderRequest.setEntity(multipart); + logger.debug("createFolder - POST URL: {}, folder name: {}", sdmUrl, parentId); try (var response = (CloseableHttpResponse) httpClient.execute(createFolderRequest)) { int responseCode = response.getStatusLine().getStatusCode(); String responseBody = EntityUtils.toString(response.getEntity()); + logger.debug("createFolder - response code: {}", responseCode); if (responseCode == 201) { logger.debug("END: createFolder - folder created successfully"); return responseBody; } else if (responseCode == 403) { throw new ServiceException(SDMUtils.getErrorMessage("USER_NOT_AUTHORISED_ERROR")); } else { + logger.error("createFolder - error response body: {}", responseBody); throw new ServiceException( SDMUtils.getErrorMessage("FAILED_TO_CREATE_FOLDER") + ". " + responseBody); } @@ -840,6 +874,11 @@ public Map copyAttachment( builder.addTextBody("succinct", "true"); HttpEntity multipart = builder.build(); uploadFile.setEntity(multipart); + logger.debug( + "copyAttachment - POST URL: {}, sourceId: {}, targetFolderId: {}", + sdmUrl, + cmisDocument.getObjectId(), + cmisDocument.getFolderId()); try (var response = (CloseableHttpResponse) httpClient.execute(uploadFile)) { // Handle response entity @@ -847,12 +886,15 @@ public Map copyAttachment( String responseBody = entity != null ? EntityUtils.toString(entity, StandardCharsets.UTF_8) : ""; - if (response.getStatusLine().getStatusCode() == 201) { + int responseCode = response.getStatusLine().getStatusCode(); + logger.debug("copyAttachment - response code: {}", responseCode); + if (responseCode == 201) { logger.debug("END: copyAttachment - copy successful"); return processCopyAttachmentResponse(responseBody, customPropertiesInSDM); } // On error, throw exception with error information + logger.error("copyAttachment - error response body: {}", responseBody); JSONObject errorJson = new JSONObject(responseBody); String exceptionType = errorJson.optString("exception"); String errorMessage = errorJson.optString("message"); diff --git a/sdm/src/main/java/com/sap/cds/sdm/utilities/SDMUtils.java b/sdm/src/main/java/com/sap/cds/sdm/utilities/SDMUtils.java index bbe5b9c15..89572bd5b 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/utilities/SDMUtils.java +++ b/sdm/src/main/java/com/sap/cds/sdm/utilities/SDMUtils.java @@ -398,9 +398,9 @@ public static Map getSecondaryPropertiesWithInvalidDefinition( title = titleAnnotation.get().getValue().toString(); } else { title = element.getName(); /* - * This is in case the user has not specified a title for the column in the cds - * file (which is optional) - */ + * This is in case the user has not specified a title for the column in the cds + * file (which is optional) + */ } invalidProperties.put(key, title); } @@ -570,7 +570,19 @@ private static void retrieveAnnotations(CdsElement cdsElement, AttachmentInfo at } private static List getKeyElementNames(CdsEntity entity) { - return entity.elements().filter(CdsElement::isKey).map(CdsElement::getName).toList(); + return entity + .elements() + .filter(CdsElement::isKey) + .map( + e -> { + String name = e.getName(); + // _drafts entity key columns have an "up__" prefix (e.g. up__bidUUID, + // up__bookItem_ID). + // The CQN where clause uses the bare field names (bidUUID, bookItem_ID). + // Strip the prefix so the contains-check in fetchUPIDFromCQN matches correctly. + return name.startsWith("up__") ? name.substring(4) : name; + }) + .toList(); } /** @@ -586,26 +598,37 @@ public static String fetchUPIDFromCQN(CqnSelect select, CdsEntity parentEntity) String upID = null; ObjectMapper mapper = new ObjectMapper(); JsonNode root = mapper.readTree(select.toString()); + logger.debug( + "fetchUPIDFromCQN - CQN from.ref: {}", root.path("SELECT").path("from").path("ref")); JsonNode refArray = root.path("SELECT").path("from").path("ref"); - JsonNode secondLast = refArray.get(refArray.size() - 2); - JsonNode whereArray; - if (secondLast != null) { - whereArray = secondLast.path("where"); - } else { - whereArray = refArray; + // Get the actual key field names from the parent entity + List keyElementNames = getKeyElementNames(parentEntity); + + // Scan ref nodes from right to left (excluding the last, which is the attachment entity + // itself) to find the first node that has a where clause containing a matching key. + // This handles deep navigation paths like: + // SourcingEvents(id=...) / items(id=...) / terms(id=...) / initialSlice / itemValue / + // attachments + // where intermediate nodes (initialSlice, itemValue) have no where clause. + JsonNode whereArray = null; + for (int r = refArray.size() - 2; r >= 0; r--) { + JsonNode refNode = refArray.get(r); + if (refNode == null) continue; + JsonNode candidate = refNode.path("where"); + if (candidate != null && !candidate.isMissingNode() && candidate.size() > 0) { + whereArray = candidate; + break; + } } - // If where condition is not present or empty, return null (valid scenario for - // select without - // filter) - if (whereArray == null || whereArray.isMissingNode() || whereArray.size() == 0) { + // If no ref node with a where clause was found, return null (valid scenario for + // select without filter) + if (whereArray == null) { + logger.debug("END: fetchUPIDFromCQN - upID: null (no where clause found in ref chain)"); return null; } - // Get the actual key field names from the parent entity - List keyElementNames = getKeyElementNames(parentEntity); - for (int i = 0; i < whereArray.size(); i++) { JsonNode node = whereArray.get(i);