Skip to content

Commit db77536

Browse files
committed
Add BaseRepository abstract class for Repository implementations
Provides default implementations for getResourceClass() and patch(), eliminating boilerplate that every Repository implementation must repeat. The patch() default uses the standard get-apply-update pattern with PatchHandler. Subclasses only need to implement create, update, get, find, and delete. Generated-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent cbf59e7 commit db77536

14 files changed

Lines changed: 385 additions & 258 deletions

File tree

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.directory.scim.core.repository;
21+
22+
import org.apache.directory.scim.spec.exception.ResourceException;
23+
import org.apache.directory.scim.spec.exception.ResourceNotFoundException;
24+
import org.apache.directory.scim.spec.patch.PatchOperation;
25+
import org.apache.directory.scim.spec.resources.ScimResource;
26+
27+
import java.util.List;
28+
29+
/**
30+
* Optional base class for {@link Repository} implementations that provides default
31+
* implementations for common boilerplate methods.
32+
*
33+
* <p>Subclasses must implement {@link #create}, {@link #update}, {@link #get},
34+
* {@link #find}, and {@link #delete}. The following are provided automatically:</p>
35+
* <ul>
36+
* <li>{@link #getResourceClass()} — returns the class passed to the constructor</li>
37+
* <li>{@link #patch(String, List, ScimRequestContext)} — fetches the current resource
38+
* via {@link #get}, applies patch operations via {@link PatchHandler}, and persists
39+
* via {@link #update}</li>
40+
* </ul>
41+
*
42+
* <p>Usage example:</p>
43+
* <pre>
44+
* &#64;Named
45+
* &#64;ApplicationScoped
46+
* public class MyUserRepository extends BaseRepository&lt;ScimUser&gt; {
47+
* &#64;Inject
48+
* public MyUserRepository(PatchHandler patchHandler) {
49+
* super(ScimUser.class, patchHandler);
50+
* }
51+
* // implement create, update, get, find, delete
52+
* }
53+
* </pre>
54+
*
55+
* @param <T> the SCIM resource type this repository manages
56+
*/
57+
public abstract class BaseRepository<T extends ScimResource> implements Repository<T> {
58+
59+
private final Class<T> resourceClass;
60+
private final PatchHandler patchHandler;
61+
62+
/**
63+
* Creates a new base repository.
64+
*
65+
* @param resourceClass the SCIM resource class this repository manages
66+
* @param patchHandler the handler used to apply SCIM PATCH operations
67+
*/
68+
protected BaseRepository(Class<T> resourceClass, PatchHandler patchHandler) {
69+
this.resourceClass = resourceClass;
70+
this.patchHandler = patchHandler;
71+
}
72+
73+
/**
74+
* No-arg constructor for CDI proxy creation. Subclasses using CDI must also
75+
* provide a no-arg constructor that calls {@code super()}.
76+
*/
77+
protected BaseRepository() {
78+
this.resourceClass = null;
79+
this.patchHandler = null;
80+
}
81+
82+
@Override
83+
public Class<T> getResourceClass() {
84+
return resourceClass;
85+
}
86+
87+
/**
88+
* Default PATCH implementation: fetches the current resource, applies the patch
89+
* operations, and persists the result via {@link #update}.
90+
*
91+
* <p>Subclasses may override this method if their backend supports more efficient
92+
* partial-update semantics.</p>
93+
*
94+
* {@inheritDoc}
95+
*/
96+
@Override
97+
public T patch(String id, List<PatchOperation> patchOperations,
98+
ScimRequestContext requestContext) throws ResourceException {
99+
if (patchHandler == null) {
100+
throw new IllegalStateException("No PatchHandler configured; patch() is not available on this instance");
101+
}
102+
T current = get(id, requestContext);
103+
if (current == null) {
104+
throw new ResourceNotFoundException(id);
105+
}
106+
T patched = patchHandler.apply(current, patchOperations);
107+
return update(id, patched, requestContext);
108+
}
109+
}
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.directory.scim.core.repository;
21+
22+
import org.apache.directory.scim.spec.exception.ResourceException;
23+
import org.apache.directory.scim.spec.exception.ResourceNotFoundException;
24+
import org.apache.directory.scim.spec.filter.Filter;
25+
import org.apache.directory.scim.spec.filter.FilterResponse;
26+
import org.apache.directory.scim.spec.patch.PatchOperation;
27+
import org.apache.directory.scim.spec.resources.ScimUser;
28+
import org.junit.jupiter.api.Test;
29+
import org.mockito.Mockito;
30+
31+
import java.util.List;
32+
33+
import static org.assertj.core.api.Assertions.assertThat;
34+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
35+
import static org.mockito.ArgumentMatchers.any;
36+
import static org.mockito.ArgumentMatchers.eq;
37+
import static org.mockito.Mockito.verify;
38+
import static org.mockito.Mockito.when;
39+
40+
class BaseRepositoryTest {
41+
42+
/**
43+
* Concrete test subclass that delegates abstract methods to mockable lambdas.
44+
*/
45+
static class TestRepository extends BaseRepository<ScimUser> {
46+
47+
TestRepository(PatchHandler patchHandler) {
48+
super(ScimUser.class, patchHandler);
49+
}
50+
51+
/** No-arg constructor to verify CDI proxy path. */
52+
TestRepository() {
53+
super();
54+
}
55+
56+
@Override
57+
public ScimUser create(ScimUser resource, ScimRequestContext requestContext) {
58+
return null;
59+
}
60+
61+
@Override
62+
public ScimUser update(String id, ScimUser resource, ScimRequestContext requestContext) {
63+
return resource;
64+
}
65+
66+
@Override
67+
public ScimUser get(String id, ScimRequestContext requestContext) {
68+
return null;
69+
}
70+
71+
@Override
72+
public FilterResponse<ScimUser> find(Filter filter, ScimRequestContext requestContext) {
73+
return null;
74+
}
75+
76+
@Override
77+
public void delete(String id) {
78+
}
79+
}
80+
81+
@Test
82+
void getResourceClass_returnsClassPassedToConstructor() {
83+
PatchHandler patchHandler = Mockito.mock(PatchHandler.class);
84+
TestRepository repository = new TestRepository(patchHandler);
85+
86+
assertThat(repository.getResourceClass()).isEqualTo(ScimUser.class);
87+
}
88+
89+
@Test
90+
void noArgConstructor_doesNotThrow() {
91+
TestRepository repository = new TestRepository();
92+
93+
assertThat(repository.getResourceClass()).isNull();
94+
}
95+
96+
@Test
97+
void patch_callsGetThenApplyThenUpdate() throws ResourceException {
98+
PatchHandler patchHandler = Mockito.mock(PatchHandler.class);
99+
TestRepository repository = Mockito.spy(new TestRepository(patchHandler));
100+
101+
ScimUser existing = new ScimUser();
102+
existing.setId("user-1");
103+
ScimUser patched = new ScimUser();
104+
patched.setId("user-1-patched");
105+
ScimUser updated = new ScimUser();
106+
updated.setId("user-1-updated");
107+
108+
List<PatchOperation> ops = List.of(new PatchOperation());
109+
ScimRequestContext ctx = ScimRequestContext.empty();
110+
111+
when(repository.get("user-1", ctx)).thenReturn(existing);
112+
when(patchHandler.apply(existing, ops)).thenReturn(patched);
113+
when(repository.update("user-1", patched, ctx)).thenReturn(updated);
114+
115+
ScimUser result = repository.patch("user-1", ops, ctx);
116+
117+
assertThat(result).isSameAs(updated);
118+
verify(repository).get("user-1", ctx);
119+
verify(patchHandler).apply(existing, ops);
120+
verify(repository).update("user-1", patched, ctx);
121+
}
122+
123+
@Test
124+
void patch_throwsResourceNotFoundExceptionWhenGetReturnsNull() throws ResourceException {
125+
PatchHandler patchHandler = Mockito.mock(PatchHandler.class);
126+
TestRepository repository = Mockito.spy(new TestRepository(patchHandler));
127+
128+
ScimRequestContext ctx = ScimRequestContext.empty();
129+
when(repository.get("missing-id", ctx)).thenReturn(null);
130+
131+
assertThatThrownBy(() -> repository.patch("missing-id", List.of(), ctx))
132+
.isInstanceOf(ResourceNotFoundException.class);
133+
}
134+
135+
@Test
136+
void patch_passesCorrectIdOperationsAndContext() throws ResourceException {
137+
PatchHandler patchHandler = Mockito.mock(PatchHandler.class);
138+
TestRepository repository = Mockito.spy(new TestRepository(patchHandler));
139+
140+
ScimUser existing = new ScimUser();
141+
ScimUser patched = new ScimUser();
142+
ScimUser updated = new ScimUser();
143+
144+
PatchOperation op1 = new PatchOperation();
145+
PatchOperation op2 = new PatchOperation();
146+
List<PatchOperation> ops = List.of(op1, op2);
147+
ScimRequestContext ctx = ScimRequestContext.empty();
148+
149+
when(repository.get("id-42", ctx)).thenReturn(existing);
150+
when(patchHandler.apply(existing, ops)).thenReturn(patched);
151+
when(repository.update("id-42", patched, ctx)).thenReturn(updated);
152+
153+
repository.patch("id-42", ops, ctx);
154+
155+
verify(repository).get(eq("id-42"), eq(ctx));
156+
verify(patchHandler).apply(eq(existing), eq(ops));
157+
verify(repository).update(eq("id-42"), eq(patched), eq(ctx));
158+
}
159+
160+
@Test
161+
void patch_returnsResultFromUpdate() throws ResourceException {
162+
PatchHandler patchHandler = Mockito.mock(PatchHandler.class);
163+
TestRepository repository = Mockito.spy(new TestRepository(patchHandler));
164+
165+
ScimUser existing = new ScimUser();
166+
ScimUser patched = new ScimUser();
167+
ScimUser updateResult = new ScimUser();
168+
updateResult.setId("final-result");
169+
170+
ScimRequestContext ctx = ScimRequestContext.empty();
171+
when(repository.get("id-1", ctx)).thenReturn(existing);
172+
when(patchHandler.apply(any(), any())).thenReturn(patched);
173+
when(repository.update(any(), any(), any())).thenReturn(updateResult);
174+
175+
ScimUser result = repository.patch("id-1", List.of(), ctx);
176+
177+
assertThat(result).isSameAs(updateResult);
178+
assertThat(result.getId()).isEqualTo("final-result");
179+
}
180+
181+
@Test
182+
void patch_doesNotCallUpdateWhenGetReturnsNull() throws ResourceException {
183+
PatchHandler patchHandler = Mockito.mock(PatchHandler.class);
184+
TestRepository repository = Mockito.spy(new TestRepository(patchHandler));
185+
186+
ScimRequestContext ctx = ScimRequestContext.empty();
187+
when(repository.get("absent", ctx)).thenReturn(null);
188+
189+
try {
190+
repository.patch("absent", List.of(), ctx);
191+
} catch (ResourceNotFoundException e) {
192+
// expected
193+
}
194+
195+
verify(repository, Mockito.never()).update(any(), any(), any());
196+
verify(patchHandler, Mockito.never()).apply(any(), any());
197+
}
198+
199+
@Test
200+
void patch_doesNotCallApplyWhenGetReturnsNull() throws ResourceException {
201+
PatchHandler patchHandler = Mockito.mock(PatchHandler.class);
202+
TestRepository repository = Mockito.spy(new TestRepository(patchHandler));
203+
204+
ScimRequestContext ctx = ScimRequestContext.empty();
205+
when(repository.get("no-such", ctx)).thenReturn(null);
206+
207+
try {
208+
repository.patch("no-such", List.of(), ctx);
209+
} catch (ResourceNotFoundException e) {
210+
// expected
211+
}
212+
213+
verify(patchHandler, Mockito.never()).apply(any(), any());
214+
}
215+
216+
@Test
217+
void patch_nullPatchHandler_throwsIllegalStateException() {
218+
TestRepository repository = Mockito.spy(new TestRepository(null));
219+
220+
assertThatThrownBy(() -> repository.patch("id", List.of(), ScimRequestContext.empty()))
221+
.isInstanceOf(IllegalStateException.class)
222+
.hasMessageContaining("No PatchHandler configured");
223+
}
224+
225+
@Test
226+
void patch_patchHandlerThrowsException_propagates() throws ResourceException {
227+
PatchHandler patchHandler = Mockito.mock(PatchHandler.class);
228+
TestRepository repository = Mockito.spy(new TestRepository(patchHandler));
229+
230+
ScimUser user = new ScimUser();
231+
user.setId("id1");
232+
ScimRequestContext ctx = ScimRequestContext.empty();
233+
when(repository.get("id1", ctx)).thenReturn(user);
234+
when(patchHandler.apply(any(), any())).thenThrow(new RuntimeException("patch failed"));
235+
236+
assertThatThrownBy(() -> repository.patch("id1", List.of(), ctx))
237+
.isInstanceOf(RuntimeException.class)
238+
.hasMessage("patch failed");
239+
}
240+
}

0 commit comments

Comments
 (0)