Skip to content

Commit e370185

Browse files
MatKuhrKavithaSivanewtork
authored
feat: [Destinations] Support Fragments (#491)
Co-authored-by: KavithaSiva <32287936+KavithaSiva@users.noreply.github.com> Co-authored-by: Alexander Dümont <22489773+newtork@users.noreply.github.com>
1 parent 4b575ff commit e370185

File tree

8 files changed

+172
-7
lines changed

8 files changed

+172
-7
lines changed

cloudplatform/connectivity-destination-service/src/main/java/com/sap/cloud/sdk/cloudplatform/connectivity/DestinationRetrievalStrategy.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ final class DestinationRetrievalStrategy
2828
@Nullable
2929
@ToString.Exclude
3030
private final String token;
31+
@Nullable
32+
private String fragment;
3133

3234
static DestinationRetrievalStrategy withoutToken( @Nonnull final OnBehalfOf behalf )
3335
{
@@ -53,6 +55,19 @@ static DestinationRetrievalStrategy withUserToken( @Nonnull final OnBehalfOf beh
5355
return new DestinationRetrievalStrategy(behalf, REFRESH_TOKEN, token);
5456
}
5557

58+
DestinationRetrievalStrategy withFragmentName( @Nonnull final String fragmentName )
59+
{
60+
if( fragmentName.isBlank() ) {
61+
throw new IllegalArgumentException("Fragment name must not be empty");
62+
}
63+
// sanity check to enforce this is only ever set once
64+
if( fragment != null ) {
65+
throw new IllegalStateException("Attempted to change an already set fragment name");
66+
}
67+
fragment = fragmentName;
68+
return this;
69+
}
70+
5671
enum TokenForwarding
5772
{
5873
USER_TOKEN,

cloudplatform/connectivity-destination-service/src/main/java/com/sap/cloud/sdk/cloudplatform/connectivity/DestinationRetrievalStrategyResolver.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,18 @@ DestinationRetrieval prepareSupplier( @Nonnull final DestinationOptions options
115115
.getRefreshToken(options)
116116
.peek(any -> log.debug("Refresh token given, applying refresh token flow."))
117117
.getOrNull();
118+
final String fragmentName =
119+
DestinationServiceOptionsAugmenter
120+
.getFragmentName(options)
121+
.peek(it -> log.debug("Found fragment name '{}'.", it))
122+
.getOrNull();
118123

119124
log
120125
.debug(
121126
"Loading destination from reuse-destination-service with retrieval strategy {} and token exchange strategy {}.",
122127
retrievalStrategy,
123128
tokenExchangeStrategy);
124-
return prepareSupplier(retrievalStrategy, tokenExchangeStrategy, refreshToken);
129+
return prepareSupplier(retrievalStrategy, tokenExchangeStrategy, refreshToken, fragmentName);
125130
}
126131

127132
/**
@@ -159,7 +164,8 @@ private DestinationServiceTokenExchangeStrategy getDefaultTokenExchangeStrategy(
159164
DestinationRetrieval prepareSupplier(
160165
@Nonnull final DestinationServiceRetrievalStrategy retrievalStrategy,
161166
@Nonnull final DestinationServiceTokenExchangeStrategy tokenExchangeStrategy,
162-
@Nullable final String refreshToken )
167+
@Nullable final String refreshToken,
168+
@Nullable final String fragmentName )
163169
throws DestinationAccessException
164170
{
165171
log
@@ -175,6 +181,9 @@ DestinationRetrieval prepareSupplier(
175181
retrievalStrategy,
176182
DestinationServiceTokenExchangeStrategy.LOOKUP_ONLY,
177183
refreshToken);
184+
if( fragmentName != null ) {
185+
strategy.withFragmentName(fragmentName);
186+
}
178187
return new DestinationRetrieval(() -> {
179188
final DestinationServiceV1Response result = destinationRetriever.apply(strategy);
180189
if( !doesDestinationConfigurationRequireUserTokenExchange(result) ) {
@@ -190,6 +199,9 @@ DestinationRetrieval prepareSupplier(
190199

191200
final DestinationRetrievalStrategy strategy =
192201
resolveSingleRequestStrategy(retrievalStrategy, tokenExchangeStrategy, refreshToken);
202+
if( fragmentName != null ) {
203+
strategy.withFragmentName(fragmentName);
204+
}
193205
return new DestinationRetrieval(() -> destinationRetriever.apply(strategy), strategy.behalf());
194206
}
195207

cloudplatform/connectivity-destination-service/src/main/java/com/sap/cloud/sdk/cloudplatform/connectivity/DestinationServiceAdapter.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,9 @@ private HttpUriRequest prepareRequest( final String servicePath, final Destinati
200200
if( headerName != null ) {
201201
request.addHeader(headerName, strategy.token());
202202
}
203+
if( strategy.fragment() != null ) {
204+
request.addHeader("x-fragment-name", strategy.fragment());
205+
}
203206
return request;
204207
}
205208

cloudplatform/connectivity-destination-service/src/main/java/com/sap/cloud/sdk/cloudplatform/connectivity/DestinationServiceOptionsAugmenter.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public class DestinationServiceOptionsAugmenter implements DestinationOptionsAug
2424
static final String DESTINATION_RETRIEVAL_STRATEGY_KEY = "scp.cf.destinationRetrievalStrategy";
2525
static final String DESTINATION_TOKEN_EXCHANGE_STRATEGY_KEY = "scp.cf.destinationTokenExchangeStrategy";
2626
static final String X_REFRESH_TOKEN_KEY = "x-refresh-token";
27+
static final String X_FRAGMENT_KEY = "X-fragment-name";
2728

2829
private final Map<String, Object> parameters = new HashMap<>();
2930

@@ -86,6 +87,31 @@ public DestinationServiceOptionsAugmenter refreshToken( @Nonnull final String re
8687
return this;
8788
}
8889

90+
/**
91+
* Fragment that should enhance the destination to be fetched.
92+
*
93+
* @param fragmentName
94+
* The fragment name.
95+
* @return The same augmenter that called this method.
96+
* @since 5.11.0
97+
*/
98+
@Beta
99+
@Nonnull
100+
public DestinationServiceOptionsAugmenter fragmentName( @Nonnull final String fragmentName )
101+
{
102+
parameters.put(X_FRAGMENT_KEY, fragmentName);
103+
if( DestinationService.Cache.isEnabled() && DestinationService.Cache.isChangeDetectionEnabled() ) {
104+
log
105+
.warn(
106+
"""
107+
A fragment was requested while change detection caching is enabled.\
108+
This is not recommended, as fragment-based destinations will effectively not be cached with this strategy.\
109+
Consider disabling change detection, if you frequently use destination fragments.
110+
""");
111+
}
112+
return this;
113+
}
114+
89115
@Override
90116
public void augmentBuilder( @Nonnull final DestinationOptions.Builder builder )
91117
{
@@ -145,4 +171,10 @@ static Option<String> getRefreshToken( @Nonnull final DestinationOptions options
145171
{
146172
return options.get(X_REFRESH_TOKEN_KEY).filter(String.class::isInstance).map(String.class::cast);
147173
}
174+
175+
@Nonnull
176+
static Option<String> getFragmentName( @Nonnull final DestinationOptions options )
177+
{
178+
return options.get(X_FRAGMENT_KEY).filter(String.class::isInstance).map(String.class::cast);
179+
}
148180
}

cloudplatform/connectivity-destination-service/src/test/java/com/sap/cloud/sdk/cloudplatform/connectivity/DestinationRetrievalStrategyResolverTest.java

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ void testExceptionsAreThrownOnIllegalCombinations()
213213
.executeWithTenant(
214214
c._3(),
215215
() -> softly
216-
.assertThatThrownBy(() -> sut.prepareSupplier(c._1(), c._2(), null))
216+
.assertThatThrownBy(() -> sut.prepareSupplier(c._1(), c._2(), null, null))
217217
.as("Expecting '%s' with '%s' and '%s' to throw.", c._1(), c._2(), c._3())
218218
.isInstanceOf(DestinationAccessException.class)));
219219

@@ -234,7 +234,12 @@ void testExceptionsAreThrownForImpossibleTokenExchanges()
234234
{
235235
doAnswer(( any ) -> true).when(sut).doesDestinationConfigurationRequireUserTokenExchange(any());
236236
final DestinationRetrieval supplier =
237-
sut.prepareSupplier(ALWAYS_PROVIDER, DestinationServiceTokenExchangeStrategy.LOOKUP_THEN_EXCHANGE, null);
237+
sut
238+
.prepareSupplier(
239+
ALWAYS_PROVIDER,
240+
DestinationServiceTokenExchangeStrategy.LOOKUP_THEN_EXCHANGE,
241+
null,
242+
null);
238243

239244
TenantAccessor
240245
.executeWithTenant(
@@ -259,7 +264,7 @@ void testDefaultStrategies()
259264
sut.prepareSupplier(DestinationOptions.builder().build());
260265
sut.prepareSupplierAllDestinations(DestinationOptions.builder().build());
261266

262-
verify(sut).prepareSupplier(CURRENT_TENANT, FORWARD_USER_TOKEN, null);
267+
verify(sut).prepareSupplier(CURRENT_TENANT, FORWARD_USER_TOKEN, null, null);
263268
verify(sut).prepareSupplierAllDestinations(CURRENT_TENANT);
264269
}
265270

@@ -272,7 +277,8 @@ void testDefaultNonXsuaaTokenStrategy()
272277
sut.prepareSupplier(DestinationOptions.builder().build());
273278
sut.prepareSupplierAllDestinations(DestinationOptions.builder().build());
274279

275-
verify(sut).prepareSupplier(CURRENT_TENANT, DestinationServiceTokenExchangeStrategy.LOOKUP_THEN_EXCHANGE, null);
280+
verify(sut)
281+
.prepareSupplier(CURRENT_TENANT, DestinationServiceTokenExchangeStrategy.LOOKUP_THEN_EXCHANGE, null, null);
276282
verify(sut).prepareSupplierAllDestinations(CURRENT_TENANT);
277283
}
278284

@@ -290,4 +296,19 @@ void testRefreshToken()
290296

291297
verify(sut).resolveSingleRequestStrategy(eq(CURRENT_TENANT), eq(LOOKUP_ONLY), eq(refreshToken));
292298
}
299+
300+
@Test
301+
void testFragmentName()
302+
{
303+
final String fragmentName = "my-fragment";
304+
final DestinationOptions opts =
305+
DestinationOptions
306+
.builder()
307+
.augmentBuilder(DestinationServiceOptionsAugmenter.augmenter().fragmentName(fragmentName))
308+
.build();
309+
310+
sut.prepareSupplier(opts);
311+
312+
verify(sut).prepareSupplier(any(), any(), eq(null), eq(fragmentName));
313+
}
293314
}

cloudplatform/connectivity-destination-service/src/test/java/com/sap/cloud/sdk/cloudplatform/connectivity/DestinationServiceAdapterTest.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,31 @@ void testRefreshTokenFlow()
244244
.withoutHeader("x-user-token"));
245245
}
246246

247+
@Test
248+
void testFragmentName()
249+
{
250+
final DestinationServiceAdapter adapterToTest = createSut(DEFAULT_SERVICE_BINDING);
251+
252+
final String fragment = "my-fragment";
253+
254+
final String destinationResponse =
255+
adapterToTest
256+
.getConfigurationAsJson(
257+
"/",
258+
DestinationRetrievalStrategy
259+
.withoutToken(TECHNICAL_USER_CURRENT_TENANT)
260+
.withFragmentName(fragment));
261+
262+
assertThat(destinationResponse).isEqualTo(DESTINATION_RESPONSE);
263+
264+
verify(
265+
1,
266+
getRequestedFor(urlEqualTo(DESTINATION_SERVICE_URL))
267+
.withHeader("Authorization", equalTo("Bearer " + xsuaaToken))
268+
.withHeader("x-fragment-name", equalTo(fragment))
269+
.withoutHeader("x-user-token"));
270+
}
271+
247272
@Test
248273
void getDestinationServiceProviderTenantShouldReturnProviderTenantFromServiceBinding()
249274
{

cloudplatform/connectivity-destination-service/src/test/java/com/sap/cloud/sdk/cloudplatform/connectivity/DestinationServiceTest.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import java.util.concurrent.TimeoutException;
5353
import java.util.concurrent.atomic.AtomicReference;
5454
import java.util.concurrent.locks.ReentrantLock;
55+
import java.util.function.Function;
5556

5657
import org.apache.http.HttpVersion;
5758
import org.apache.http.client.HttpClient;
@@ -1598,6 +1599,60 @@ void testAuthTokenFailureIsNotCached()
15981599
withUserToken(TECHNICAL_USER_CURRENT_TENANT, userToken));
15991600
}
16001601

1602+
@Test
1603+
void testFragmentDestinationsAreCacheIsolated()
1604+
{
1605+
DestinationService.Cache.disableChangeDetection();
1606+
final String destinationTemplate = """
1607+
{
1608+
"owner": {
1609+
"SubaccountId": "00000000-0000-0000-0000-000000000000",
1610+
"InstanceId": null
1611+
},
1612+
"destinationConfiguration": {
1613+
"Name": "destination",
1614+
%s
1615+
"Type": "HTTP",
1616+
"URL": "https://%s.com/",
1617+
"Authentication": "NoAuthentication",
1618+
"ProxyType": "Internet"
1619+
}
1620+
}
1621+
""";
1622+
1623+
doReturn(destinationTemplate.formatted("\"FragmentName\": \"a-fragment\",", "a.fragment"))
1624+
.when(destinationServiceAdapter)
1625+
.getConfigurationAsJson(any(), argThat(it -> "a-fragment".equals(it.fragment())));
1626+
doReturn(destinationTemplate.formatted("\"FragmentName\": \"b-fragment\",", "b.fragment"))
1627+
.when(destinationServiceAdapter)
1628+
.getConfigurationAsJson(any(), argThat(it -> "b-fragment".equals(it.fragment())));
1629+
doReturn(destinationTemplate.formatted("", "destination"))
1630+
.when(destinationServiceAdapter)
1631+
.getConfigurationAsJson(any(), argThat(it -> it.fragment() == null));
1632+
1633+
final Function<String, DestinationOptions> optsBuilder =
1634+
frag -> DestinationOptions
1635+
.builder()
1636+
.augmentBuilder(DestinationServiceOptionsAugmenter.augmenter().fragmentName(frag))
1637+
.build();
1638+
1639+
final Destination dA = loader.tryGetDestination("destination", optsBuilder.apply("a-fragment")).get();
1640+
final Destination dB = loader.tryGetDestination("destination", optsBuilder.apply("b-fragment")).get();
1641+
final Destination d = loader.tryGetDestination("destination").get();
1642+
1643+
assertThat(dA).isNotEqualTo(dB).isNotEqualTo(d);
1644+
assertThat(dA.get("FragmentName")).contains("a-fragment");
1645+
assertThat(dB).isNotEqualTo(d);
1646+
assertThat(dB.get("FragmentName")).contains("b-fragment");
1647+
1648+
assertThat(d.get("FragmentName")).isEmpty();
1649+
1650+
assertThat(dA)
1651+
.describedAs("Destinations with fragments should be cached")
1652+
.isSameAs(loader.tryGetDestination("destination", optsBuilder.apply("a-fragment")).get());
1653+
verify(destinationServiceAdapter, times(3)).getConfigurationAsJson(any(), any());
1654+
}
1655+
16011656
// @Test
16021657
// Performance test is unreliable on Jenkins
16031658
void runLoadTest()

release_notes.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212

1313
### ✨ New Functionality
1414

15-
-
15+
- Add experimental support for [_Destination Fragments_](https://help.sap.com/docs/connectivity/sap-btp-connectivity-cf/extending-destinations-with-fragments).
16+
Fragment names can be passed upon requesting destinations via `DestinationServiceOptionsAugmenter.fragmentName("my-fragment-name")`.
17+
For further details refer to [the documentation]().
1618

1719
### 📈 Improvements
1820

0 commit comments

Comments
 (0)