Skip to content

Commit d4a3f20

Browse files
authored
fix: avoid mutating resource paths in ODataRequestCount (#1121)
1 parent 8aa4216 commit d4a3f20

10 files changed

Lines changed: 124 additions & 30 deletions

File tree

datamodel/odata-client/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/expression/ODataResourcePath.java

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.sap.cloud.sdk.datamodel.odata.client.expression;
22

33
import java.util.ArrayList;
4+
import java.util.Collections;
45
import java.util.List;
56
import java.util.stream.Collectors;
67

@@ -35,7 +36,20 @@ public final class ODataResourcePath
3536
*/
3637
@Getter( AccessLevel.PUBLIC )
3738
@Nonnull
38-
private final List<Tuple2<String, AbstractODataParameters>> segments = new ArrayList<>();
39+
private final List<Tuple2<String, AbstractODataParameters>> segments;
40+
41+
/**
42+
* Default constructor to create an empty resource path.
43+
*/
44+
public ODataResourcePath()
45+
{
46+
this(Collections.emptyList());
47+
}
48+
49+
private ODataResourcePath( @Nonnull final List<Tuple2<String, AbstractODataParameters>> segments )
50+
{
51+
this.segments = List.copyOf(segments);
52+
}
3953

4054
/**
4155
* Convenience method for {@code new ODataResourcePath().addSegment(segment)}. It creates a new resource path for
@@ -97,8 +111,9 @@ public ODataResourcePath addSegment( @Nonnull final String segment )
97111
ODataResourcePath
98112
addSegment( @Nonnull final String segment, @Nullable final AbstractODataParameters parameters )
99113
{
100-
segments.add(Tuple.of(segment, parameters));
101-
return this;
114+
final var newSegments = new ArrayList<>(segments);
115+
newSegments.add(Tuple.of(segment, parameters));
116+
return new ODataResourcePath(newSegments);
102117
}
103118

104119
/**
@@ -128,8 +143,9 @@ public ODataResourcePath addParameterToLastSegment( @Nonnull final AbstractOData
128143
lastSegment._2());
129144
throw new IllegalStateException(msg);
130145
}
131-
segments.set(segments.size() - 1, lastSegment.update2(parameters));
132-
return this;
146+
final var newSegments = new ArrayList<>(segments);
147+
newSegments.set(newSegments.size() - 1, lastSegment.update2(parameters));
148+
return new ODataResourcePath(newSegments);
133149
}
134150

135151
/**

datamodel/odata-client/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestFunction.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -171,9 +171,7 @@ private static ODataResourcePath appendResourcePathWithParameters(
171171
if( protocol.isEqualTo(ODataProtocol.V2) ) {
172172
return path;
173173
}
174-
final ODataResourcePath appendedPath = new ODataResourcePath();
175-
path.getSegments().forEach(s -> appendedPath.addSegment(s._1, s._2));
176-
return appendedPath.addParameterToLastSegment(parameters);
174+
return path.addParameterToLastSegment(parameters);
177175
}
178176

179177
@Nullable

datamodel/odata-client/src/test/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataQueryReadByKeyTest.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,4 +144,16 @@ void testConstructorWithStructuredQuery()
144144
assertThat(actual).isEqualTo(expected);
145145
assertThat(actual.getQueryString()).isEqualTo(expected.getQueryString());
146146
}
147+
148+
@Test
149+
void testConstructorWithStructuredQueryDoesNotMutateResourcePath()
150+
{
151+
final StructuredQuery structuredQuery =
152+
StructuredQuery.onEntity(ENTITY_COLLECTION, ODataProtocol.V4).withInlineCount();
153+
final ODataResourcePath resourcePath = ODataResourcePath.of(ENTITY_COLLECTION);
154+
155+
new ODataRequestReadByKey(SERVICE_PATH, resourcePath, ENTITY_KEY, structuredQuery);
156+
157+
assertThat(resourcePath.toString()).isEqualTo("/" + ENTITY_COLLECTION);
158+
}
147159
}

datamodel/odata-client/src/test/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestCountTest.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,14 @@ void testConstructorWithStructuredQuery()
9292
assertThat(actual).isEqualTo(expected);
9393
assertThat(actual.getQueryString()).isEqualTo(expected.getQueryString());
9494
}
95+
96+
@Test
97+
void testConstructorDoesNotMutateResourcePath()
98+
{
99+
final ODataResourcePath resourcePath = ODataResourcePath.of(ENTITY_NAME);
100+
101+
new ODataRequestCount(SERVICE_PATH, resourcePath, "", ODataProtocol.V4);
102+
103+
assertThat(resourcePath.toString()).isEqualTo("/" + ENTITY_NAME);
104+
}
95105
}

datamodel/odata-client/src/test/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestFunctionTest.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,4 +203,27 @@ void testConstructorWithStructuredQuery()
203203
.hasParameter("$orderby", "name asc,ID asc")
204204
.hasParameter("$count", "true");
205205
}
206+
207+
@Test
208+
void testConstructorWithStructuredQueryDoesNotMutateResourcePath()
209+
{
210+
final StructuredQuery structuredQuery = StructuredQuery.onEntity("Authors", ODataProtocol.V4).withInlineCount();
211+
final ODataResourcePath functionPath = ODataResourcePath.of(ODATA_FUNCTION);
212+
213+
new ODataRequestFunction(ODATA_SERVICE_PATH, functionPath, structuredQuery);
214+
215+
assertThat(functionPath.toString()).isEqualTo("/" + ODATA_FUNCTION);
216+
}
217+
218+
@Test
219+
void testParameterHandlingDoesNotMutateResourcePath()
220+
{
221+
final ODataFunctionParameters parameters =
222+
new ODataFunctionParameters(ODataProtocol.V4).addParameter("key", "val");
223+
final ODataResourcePath functionPath = ODataResourcePath.of(ODATA_FUNCTION);
224+
225+
new ODataRequestFunction(ODATA_SERVICE_PATH, functionPath, parameters, null, ODataProtocol.V4);
226+
227+
assertThat(functionPath.toString()).isEqualTo("/" + ODATA_FUNCTION);
228+
}
206229
}

datamodel/odata-client/src/test/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestReadTest.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,4 +306,15 @@ void testConstructorWithStructuredQuery()
306306
assertThat(actual).isEqualTo(expected);
307307
assertThat(actual.getQueryString()).isEqualTo(expected.getQueryString());
308308
}
309+
310+
@Test
311+
void testConstructorWithStructuredQueryDoesNotMutateResourcePath()
312+
{
313+
final StructuredQuery structuredQuery = StructuredQuery.onEntity("Authors", ODataProtocol.V4).withInlineCount();
314+
final ODataResourcePath resourcePath = ODataResourcePath.of("Authors");
315+
316+
new ODataRequestRead("/some/service/path", resourcePath, structuredQuery);
317+
318+
assertThat(resourcePath.toString()).isEqualTo("/Authors");
319+
}
309320
}

datamodel/odata-v4/odata-v4-core/src/main/java/com/sap/cloud/sdk/datamodel/odatav4/core/ServiceWithNavigableEntitiesImpl.java

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -65,17 +65,22 @@ public DeleteRequestBuilder<EntityT> delete()
6565
public <NavigationT extends VdmEntity<NavigationT>> NavigableEntityCollection<NavigationT> navigateTo(
6666
@Nonnull final NavigationProperty.Collection<EntityT, NavigationT> property )
6767
{
68-
entityPath.addSegment(property.getFieldName());
69-
return new EntityCollection<>(servicePath, entityPath, property.getItemType());
68+
return new EntityCollection<>(
69+
servicePath,
70+
entityPath.addSegment(property.getFieldName()),
71+
property.getItemType());
7072
}
7173

7274
@Nonnull
7375
@Override
7476
public <NavigationT extends VdmEntity<NavigationT>> NavigableEntitySingle<NavigationT> navigateTo(
7577
@Nonnull final NavigationProperty.Single<EntityT, NavigationT> property )
7678
{
77-
entityPath.addSegment(property.getFieldName());
78-
return new EntitySingle<>(servicePath, entityPath, Option.none(), property.getItemType());
79+
return new EntitySingle<>(
80+
servicePath,
81+
entityPath.addSegment(property.getFieldName()),
82+
Option.none(),
83+
property.getItemType());
7984
}
8085

8186
@Nonnull
@@ -99,29 +104,34 @@ public <ResultT> CollectionValueFunctionRequestBuilder<ResultT> applyFunction(
99104
public <ResultT extends VdmEntity<ResultT>> NavigableEntitySingle<ResultT> withFunction(
100105
@Nonnull final BoundFunction.SingleToSingleEntity.Composable<EntityT, ResultT> function )
101106
{
102-
entityPath.addSegment(function.getQualifiedName(), function.getParameters());
103-
return new EntitySingle<>(servicePath, entityPath, Option.none(), function.getReturnType());
107+
return new EntitySingle<>(
108+
servicePath,
109+
entityPath.addSegment(function.getQualifiedName(), function.getParameters()),
110+
Option.none(),
111+
function.getReturnType());
104112
}
105113

106114
@Override
107115
@Nonnull
108116
public <ResultT extends VdmEntity<ResultT>> NavigableEntityCollection<ResultT> withFunction(
109117
@Nonnull final BoundFunction.SingleToCollectionEntity.Composable<EntityT, ResultT> function )
110118
{
111-
entityPath.addSegment(function.getQualifiedName(), function.getParameters());
112-
return new EntityCollection<>(servicePath, entityPath, function.getReturnType());
119+
return new EntityCollection<>(
120+
servicePath,
121+
entityPath.addSegment(function.getQualifiedName(), function.getParameters()),
122+
function.getReturnType());
113123
}
114124

115125
@Override
116126
@Nonnull
117127
public <ResultT> SingleValueActionRequestBuilder<ResultT> applyAction(
118128
@Nonnull final BoundAction.SingleToSingle<EntityT, ResultT> action )
119129
{
120-
entityPath.addSegment(action.getQualifiedName());
130+
final ODataResourcePath actionPath = entityPath.addSegment(action.getQualifiedName());
121131
final SingleValueActionRequestBuilder<ResultT> requestBuilder =
122132
new SingleValueActionRequestBuilder<>(
123133
servicePath,
124-
entityPath,
134+
actionPath,
125135
action.getParameters(),
126136
action.getReturnType());
127137
maybeEntity
@@ -138,11 +148,11 @@ public <ResultT> SingleValueActionRequestBuilder<ResultT> applyAction(
138148
public <ResultT> CollectionValueActionRequestBuilder<ResultT> applyAction(
139149
@Nonnull final BoundAction.SingleToCollection<EntityT, ResultT> action )
140150
{
141-
entityPath.addSegment(action.getQualifiedName());
151+
final ODataResourcePath actionPath = entityPath.addSegment(action.getQualifiedName());
142152
final CollectionValueActionRequestBuilder<ResultT> requestBuilder =
143153
new CollectionValueActionRequestBuilder<>(
144154
servicePath,
145-
entityPath,
155+
actionPath,
146156
action.getParameters(),
147157
action.getReturnType());
148158
maybeEntity
@@ -170,8 +180,11 @@ static class EntityCollection<NavigationT extends VdmEntity<NavigationT>>
170180
public <EntityT extends VdmEntity<EntityT>> NavigableEntitySingle<EntityT> forEntity(
171181
@Nonnull final EntityT entity )
172182
{
173-
entityPath.addParameterToLastSegment(entity.getKey());
174-
return new EntitySingle<>(servicePath, entityPath, Option.of(entity), entity.getType());
183+
return new EntitySingle<>(
184+
servicePath,
185+
entityPath.addParameterToLastSegment(entity.getKey()),
186+
Option.of(entity),
187+
entity.getType());
175188
}
176189

177190
@Override
@@ -202,10 +215,9 @@ public CountRequestBuilder<NavigationT> count()
202215
NavigableEntitySingle<ResultT>
203216
withFunction( @Nonnull final BoundFunction.CollectionToSingleEntity.Composable<EntityT, ResultT> function )
204217
{
205-
entityPath.addSegment(function.getQualifiedName(), function.getParameters());
206218
return new ServiceWithNavigableEntitiesImpl.EntitySingle<>(
207219
servicePath,
208-
entityPath,
220+
entityPath.addSegment(function.getQualifiedName(), function.getParameters()),
209221
Option.none(),
210222
function.getReturnType());
211223
}
@@ -218,10 +230,9 @@ public CountRequestBuilder<NavigationT> count()
218230
withFunction(
219231
@Nonnull final BoundFunction.CollectionToCollectionEntity.Composable<EntityT, ResultT> function )
220232
{
221-
entityPath.addSegment(function.getQualifiedName(), function.getParameters());
222233
return new ServiceWithNavigableEntitiesImpl.EntityCollection<>(
223234
servicePath,
224-
entityPath,
235+
entityPath.addSegment(function.getQualifiedName(), function.getParameters()),
225236
function.getReturnType());
226237
}
227238

datamodel/odata-v4/odata-v4-core/src/test/java/com/sap/cloud/sdk/datamodel/odatav4/core/NestedEntityTest.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,19 @@ void testCountNestedTrip()
174174
assertThat(count.getRelativeUri()).hasToString("/TripPinServiceRW/People('russellwhyte')/Trips/$count");
175175
}
176176

177+
@Test
178+
void testCountNestedTripToRequestIsStable()
179+
{
180+
final Person personByKey = Person.builder().userName("russellwhyte").build();
181+
final CountRequestBuilder<Trip> countBuilder =
182+
service.forEntity(personByKey).navigateTo(Person.TO_TRIPS).count();
183+
184+
assertThat(countBuilder.toRequest().getRelativeUri())
185+
.hasToString("/TripPinServiceRW/People('russellwhyte')/Trips/$count");
186+
assertThat(countBuilder.toRequest().getRelativeUri())
187+
.hasToString("/TripPinServiceRW/People('russellwhyte')/Trips/$count");
188+
}
189+
177190
@Test
178191
void testGetByKeyNestedPlanItem()
179192
{

datamodel/odata/odata-core/src/main/java/com/sap/cloud/sdk/datamodel/odata/helper/FluentHelperCreate.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,8 @@ public ODataRequestCreate toRequest()
8686
if( linkFromParentEntity != null && parentEntity != null ) {
8787
resourcePath =
8888
ODataResourcePath
89-
.of(parentEntity.getEntityCollection(), ODataEntityKey.of(parentEntity.getKey(), ODataProtocol.V2));
90-
resourcePath.addSegment(linkFromParentEntity.getFieldName());
89+
.of(parentEntity.getEntityCollection(), ODataEntityKey.of(parentEntity.getKey(), ODataProtocol.V2))
90+
.addSegment(linkFromParentEntity.getFieldName());
9191
} else {
9292
resourcePath = ODataResourcePath.of(getEntityCollection());
9393
}

release_notes.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
### 🔧 Compatibility Notes
1010

11-
-
11+
- `ODataResourcePath#addSegment(...)` and `addParameterToLastSegment(...)` now return a new path instance instead of mutating the existing one. Custom extensions that relied on in-place mutation need to reassign the returned path.
1212

1313
### ✨ New Functionality
1414

@@ -20,4 +20,4 @@
2020

2121
### 🐛 Fixed Issues
2222

23-
-
23+
- Fixed stateful OData request path construction caused by shared `ODataResourcePath` instances being mutated when building count, read-by-key, and function requests.

0 commit comments

Comments
 (0)