Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions .github/actions/newrelease/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion .github/workflows/main-build-and-deploy-oss.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ on:
types: [ "released" ]

permissions:
contents: read
contents: write

jobs:

Expand Down
1 change: 1 addition & 0 deletions cap-notebook/version.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1.10.2
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
</developers>

<properties>
<revision>1.10.1-SNAPSHOT</revision>
<revision>1.10.3.2-SNAPSHOT</revision>
<java.version>21</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
Expand Down
72 changes: 66 additions & 6 deletions sdm/src/main/java/com/sap/cds/sdm/handler/TokenHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,24 +56,35 @@ public String toString(byte[] bytes) {
}

public SDMCredentials getSDMCredentials() {
logger.debug("START: getSDMCredentials - loading SDM credentials from service binding");
Map<String, Object> uaaCredentials = getUaaCredentials();
Map<String, Object> uaa = (Map<String, Object>) uaaCredentials.get("uaa");
SDMCredentials sdmCredentials = new SDMCredentials();
sdmCredentials.setBaseTokenUrl(uaa.get("url").toString());
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<String, Object> getUaaCredentials() {
logger.debug("START: getUaaCredentials - scanning service bindings for 'sdm' tag");
List<ServiceBinding> 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();
}

Expand All @@ -83,37 +94,55 @@ public HttpClient getHttpClient(
String subdomain,
String type) {

logger.debug(
"START: getHttpClient - type: {}, subdomain: {}, connectionPoolConfig: {}",
type,
subdomain,
connectionPoolConfig != null ? "configured" : "null(using defaults)");

Map<String, Object> 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();
}

Map<String, Object> uaa = (Map<String, Object>) uaaCredentials.get("uaa");

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(
Expand All @@ -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<HttpDestination> destinations = getHttpDestination(user);
if (destinations.isPresent()) {
logger.debug("getHttpClientForAuthoritiesFlow - HttpDestination resolved for user: {}", user);
DefaultHttpClientFactory.DefaultHttpClientFactoryBuilder builder =
DefaultHttpClientFactory.builder();

Expand All @@ -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<HttpDestination> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -147,6 +147,21 @@ public void processBefore(CdsReadEventContext context) throws IOException {
repoValue.getIsAsyncVirusScanEnabled());
Optional<CdsEntity> attachmentDraftEntity =
context.getModel().findEntity(context.getTarget().getQualifiedName() + "_drafts");
logger.debug(
"Draft entity: {}",
attachmentDraftEntity.isPresent()
? attachmentDraftEntity.get().getQualifiedName()
: "No draft entity");
Optional<CdsEntity> 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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ private static void processAttachmentPaths(
List<String> 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
Expand Down Expand Up @@ -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;
}
Expand All @@ -284,26 +285,62 @@ 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());
}
return null;
}

private static String findPathToEntity(
CdsEntity fromEntity, String toEntityQN, Set<String> 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<Map<String, Object>> findNestedAttachments(
Map<String, Object> entity, String attachmentKey, String parentKey, String currentParentKey) {
List<Map<String, Object>> result = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <code>true</code> if the entity is a media entity, <code>false</code> 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;
}
Expand Down
6 changes: 6 additions & 0 deletions sdm/src/main/java/com/sap/cds/sdm/persistence/DBQuery.java
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,11 @@ public Map<String, String> getPropertiesForID(
PersistenceService persistenceService,
String id,
Map<String, String> 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]))
Expand All @@ -571,6 +576,7 @@ public Map<String, String> 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;
}

Expand Down
Loading
Loading