Skip to content

Commit aa45626

Browse files
committed
JPA, Mongo and S3 stores support entities with generated content id values
1 parent 3c63799 commit aa45626

File tree

7 files changed

+826
-48
lines changed

7 files changed

+826
-48
lines changed

spring-content-jpa/src/main/java/internal/org/springframework/content/jpa/repository/DefaultJpaStoreImpl.java

+26-6
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import java.io.Serializable;
99
import java.lang.annotation.Annotation;
1010
import java.lang.reflect.Field;
11-
import java.util.UUID;
1211

1312
import org.apache.commons.io.IOUtils;
1413
import org.apache.commons.logging.Log;
@@ -21,6 +20,8 @@
2120
import org.springframework.content.commons.repository.ContentStore;
2221
import org.springframework.content.commons.repository.Store;
2322
import org.springframework.content.commons.repository.StoreAccessException;
23+
import org.springframework.content.commons.store.ContentIdGeneratorManager;
24+
import org.springframework.content.commons.store.ValueGenerator;
2425
import org.springframework.content.commons.utils.BeanUtils;
2526
import org.springframework.content.commons.utils.Condition;
2627
import org.springframework.content.jpa.io.BlobResource;
@@ -39,8 +40,11 @@ public class DefaultJpaStoreImpl<S, SID extends Serializable>
3940

4041
private ResourceLoader loader;
4142

43+
private ContentIdGeneratorManager generatorManager;
44+
4245
public DefaultJpaStoreImpl(ResourceLoader blobResourceLoader) {
4346
this.loader = blobResourceLoader;
47+
this.generatorManager = new ContentIdGeneratorManager();
4448
}
4549

4650
@Override
@@ -102,14 +106,30 @@ public InputStream getContent(S entity) {
102106
@Transactional
103107
@Override
104108
public S setContent(S entity, InputStream content) {
109+
110+
ValueGenerator<Object, Serializable> generator;
111+
try {
112+
generator = generatorManager.generator(entity.getClass());
113+
} catch (InstantiationException | IllegalAccessException e) {
114+
logger.error(format("Error instantiating GenericGenerator for entity class %s", entity.getClass()), e);
115+
throw new StoreAccessException(format("Error instantiating GenericGenerator for entity class %s", entity.getClass()), e);
116+
}
117+
118+
Object contentId = BeanUtils.getFieldWithAnnotation(entity, ContentId.class);
119+
if (contentId == null || generator.regenerate(entity)) {
120+
121+
Serializable newId = generator.generate(entity);
122+
123+
Object convertedId = convertToExternalContentIdType(entity, newId);
124+
125+
BeanUtils.setFieldWithAnnotation(entity, ContentId.class, convertedId);
126+
}
127+
105128
Resource resource = getResource(entity);
106129
if (resource == null) {
107-
UUID contentId = UUID.randomUUID();
108-
Object convertedId = convertToExternalContentIdType(entity, contentId);
109-
resource = this.getResource((SID)convertedId);
110-
BeanUtils.setFieldWithAnnotation(entity, ContentId.class,
111-
convertedId);
130+
return entity;
112131
}
132+
113133
OutputStream os = null;
114134
long contentLen = -1L;
115135
try {

spring-content-jpa/src/test/java/internal/org/springframework/content/jpa/ContentStoreIT.java

+121-28
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.It;
88
import static internal.org.springframework.content.jpa.StoreIT.getContextName;
99
import static org.hamcrest.CoreMatchers.is;
10+
import static org.hamcrest.CoreMatchers.not;
1011
import static org.hamcrest.MatcherAssert.assertThat;
1112
import static org.hamcrest.Matchers.greaterThan;
1213
import static org.hamcrest.Matchers.notNullValue;
@@ -16,12 +17,25 @@
1617
import java.io.ByteArrayInputStream;
1718
import java.io.IOException;
1819
import java.io.InputStream;
20+
import java.util.UUID;
1921
import java.util.function.Supplier;
2022

23+
import javax.persistence.Entity;
24+
import javax.persistence.GeneratedValue;
25+
import javax.persistence.GenerationType;
26+
import javax.persistence.Id;
27+
import javax.persistence.Table;
28+
2129
import org.apache.commons.io.IOUtils;
2230
import org.junit.Assert;
2331
import org.junit.runner.RunWith;
32+
import org.springframework.content.commons.annotations.ContentId;
33+
import org.springframework.content.commons.annotations.ContentLength;
34+
import org.springframework.content.commons.annotations.GenericGenerator;
35+
import org.springframework.content.commons.repository.ContentStore;
36+
import org.springframework.content.commons.store.ValueGenerator;
2437
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
38+
import org.springframework.data.jpa.repository.JpaRepository;
2539
import org.springframework.transaction.PlatformTransactionManager;
2640
import org.springframework.transaction.TransactionStatus;
2741
import org.springframework.transaction.support.DefaultTransactionDefinition;
@@ -39,21 +53,24 @@
3953
import internal.org.springframework.content.jpa.testsupport.models.ClaimForm;
4054
import internal.org.springframework.content.jpa.testsupport.repositories.ClaimRepository;
4155
import internal.org.springframework.content.jpa.testsupport.stores.ClaimFormStore;
56+
import lombok.Getter;
57+
import lombok.NoArgsConstructor;
58+
import lombok.Setter;
4259

4360
@RunWith(Ginkgo4jRunner.class)
4461
@Ginkgo4jConfiguration(threads = 1)
4562
public class ContentStoreIT {
4663

4764
private static Class<?>[] CONFIG_CLASSES = new Class[]{
48-
H2Config.class,
49-
HSQLConfig.class,
50-
MySqlConfig.class,
51-
PostgresConfig.class,
65+
H2Config.class,
66+
HSQLConfig.class,
67+
MySqlConfig.class,
68+
PostgresConfig.class,
5269
SqlServerConfig.class
5370
};
5471

5572
private AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
56-
73+
5774
// for postgres (large object api operations must be in a transaction)
5875
private PlatformTransactionManager ptm;
5976

@@ -67,41 +84,41 @@ public class ContentStoreIT {
6784
Describe("ContentStore", () -> {
6885

6986
for (Class<?> configClass : CONFIG_CLASSES) {
70-
87+
7188
Context(getContextName(configClass), () -> {
72-
89+
7390
BeforeEach(() -> {
7491
context = new AnnotationConfigApplicationContext();
7592
context.register(TestConfig.class);
7693
context.register(configClass);
7794
context.refresh();
78-
95+
7996
ptm = context.getBean(PlatformTransactionManager.class);
8097
claimRepo = context.getBean(ClaimRepository.class);
8198
claimFormStore = context.getBean(ClaimFormStore.class);
82-
99+
83100
if (ptm == null) {
84101
ptm = mock(PlatformTransactionManager.class);
85102
}
86103
});
87-
104+
88105
AfterEach(() -> {
89106
deleteAllClaimFormsContent();
90107
deleteAllClaims();
91108
});
92-
109+
93110
Context("given an Entity with content", () -> {
94-
111+
95112
BeforeEach(() -> {
96113
claim = new Claim();
97114
claim.setFirstName("John");
98115
claim.setLastName("Smith");
99116
claim.setClaimForm(new ClaimForm());
100117
claim = claimRepo.save(claim);
101-
118+
102119
claimFormStore.setContent(claim.getClaimForm(), new ByteArrayInputStream("Hello Spring Content World!".getBytes()));
103120
});
104-
121+
105122
It("should be able to store new content", () -> {
106123
doInTransaction(ptm, () -> {
107124
try (InputStream content = claimFormStore.getContent(claim.getClaimForm())) {
@@ -110,32 +127,32 @@ public class ContentStoreIT {
110127
return null;
111128
});
112129
});
113-
130+
114131
It("should have content metadata", () -> {
115132
Assert.assertThat(claim.getClaimForm().getContentId(), is(notNullValue()));
116133
Assert.assertThat(claim.getClaimForm().getContentId().trim().length(), greaterThan(0));
117134
Assert.assertEquals(claim.getClaimForm().getContentLength(), 27L);
118135
});
119-
136+
120137
Context("when content is updated", () -> {
121138
BeforeEach(() ->{
122139
claimFormStore.setContent(claim.getClaimForm(), new ByteArrayInputStream("Hello Updated Spring Content World!".getBytes()));
123140
claim = claimRepo.save(claim);
124141
});
125-
142+
126143
It("should have the updated content", () -> {
127144
doInTransaction(ptm, () -> {
128145
boolean matches = false;
129146
try (InputStream content = claimFormStore.getContent(claim.getClaimForm())) {
130147
matches = IOUtils.contentEquals(new ByteArrayInputStream("Hello Updated Spring Content World!".getBytes()), content);
131148
assertThat(matches, is(true));
132149
} catch (IOException e) {
133-
}
150+
}
134151
return null;
135152
});
136153
});
137154
});
138-
155+
139156
Context("when content is updated with shorter content", () -> {
140157
BeforeEach(() -> {
141158
claimFormStore.setContent(claim.getClaimForm(), new ByteArrayInputStream("Hello Spring World!".getBytes()));
@@ -148,39 +165,78 @@ public class ContentStoreIT {
148165
matches = IOUtils.contentEquals(new ByteArrayInputStream("Hello Spring World!".getBytes()), content);
149166
assertThat(matches, is(true));
150167
} catch (IOException e) {
151-
}
168+
}
152169
return null;
153170
});
154171
});
155172
});
156-
173+
157174
Context("when content is deleted", () -> {
158175
BeforeEach(() -> {
159176
id = claim.getClaimForm().getContentId();
160177
claimFormStore.unsetContent(claim.getClaimForm());
161178
claim = claimRepo.save(claim);
162179
});
163-
180+
164181
AfterEach(() -> {
165182
claimRepo.delete(claim);
166183
});
167-
184+
168185
It("should have no content", () -> {
169186
ClaimForm deletedClaimForm = new ClaimForm();
170187
deletedClaimForm.setContentId((String)id);
171-
188+
172189
doInTransaction(ptm, () -> {
173190
try (InputStream content = claimFormStore.getContent(deletedClaimForm)) {
174191
Assert.assertThat(content, is(nullValue()));
175192
} catch (IOException e) {
176193
}
177-
return null;
194+
return null;
178195
});
179-
196+
180197
Assert.assertThat(claim.getClaimForm().getContentId(), is(nullValue()));
181198
Assert.assertEquals(claim.getClaimForm().getContentLength(), 0);
182199
});
183200
});
201+
202+
Context("when content is updated and the content id field is computed from a custom value generator", () -> {
203+
204+
It("should assign a new content Id", () -> {
205+
206+
TEntityWithGenRepository repoWithGen = context.getBean(TEntityWithGenRepository.class);
207+
TEntityWithGenStore storeWithGen = context.getBean(TEntityWithGenStore.class);
208+
209+
ContentStoreIT.TEntityWithGenerator entity = new ContentStoreIT.TEntityWithGenerator();
210+
entity = storeWithGen.setContent(entity, new ByteArrayInputStream("Hello Spring Content World!".getBytes()));
211+
entity = repoWithGen.save(entity);
212+
String firstContentId = entity.getContentId();
213+
214+
entity = storeWithGen.setContent(entity, new ByteArrayInputStream("Hello Spring Content World!".getBytes()));
215+
entity = repoWithGen.save(entity);
216+
String secondContentId = entity.getContentId();
217+
218+
assertThat(firstContentId, is(not(secondContentId)));
219+
});
220+
});
221+
222+
Context("when content is updated and the content id field is not computed", () -> {
223+
224+
It("should assign a new content Id", () -> {
225+
226+
claim = new Claim();
227+
claim.setClaimForm(new ClaimForm());
228+
claim = claimRepo.save(claim);
229+
claimFormStore.setContent(claim.getClaimForm(), new ByteArrayInputStream("Hello Spring Content World!".getBytes()));
230+
claim = claimRepo.save(claim);
231+
String firstContentId = claim.getClaimForm().getContentId();
232+
233+
claimFormStore.setContent(claim.getClaimForm(), new ByteArrayInputStream("Hello Spring Content World!".getBytes()));
234+
claim = claimRepo.save(claim);
235+
String secondContentId = claim.getClaimForm().getContentId();
236+
237+
assertThat(firstContentId, is(secondContentId));
238+
});
239+
});
184240
});
185241
});
186242
}
@@ -197,7 +253,7 @@ public static <T> T doInTransaction(PlatformTransactionManager ptm, Supplier<T>
197253
} catch (Exception e) {
198254
ptm.rollback(status);
199255
}
200-
256+
201257
return null;
202258
}
203259

@@ -206,7 +262,7 @@ protected boolean hasContent(ClaimForm claimForm) {
206262
if (claimForm == null) {
207263
return false;
208264
}
209-
265+
210266
boolean exists = doInTransaction(ptm, () -> {
211267
try (InputStream content = claimFormStore.getContent(claimForm)) {
212268
if (content != null) {
@@ -248,4 +304,41 @@ protected void deleteAllClaimFormsContent() {
248304
}
249305
}
250306
}
307+
308+
@Entity
309+
@Getter
310+
@Setter
311+
@NoArgsConstructor
312+
@Table(name="tentity_with_generator")
313+
public static class TEntityWithGenerator {
314+
315+
@Id
316+
@GeneratedValue(strategy=GenerationType.AUTO)
317+
private Long id;
318+
319+
@ContentId
320+
@GenericGenerator(strategy=ContentStoreIT.TestContentIdGenerator.class)
321+
private String contentId;
322+
323+
@ContentLength
324+
private long contentLen;
325+
}
326+
327+
public interface TEntityWithGenRepository extends JpaRepository<TEntityWithGenerator, String> {}
328+
public interface TEntityWithGenStore extends ContentStore<TEntityWithGenerator, String> {}
329+
330+
public static class TestContentIdGenerator implements ValueGenerator<ContentStoreIT.TEntityWithGenerator, String> {
331+
332+
@Override
333+
public String generate(TEntityWithGenerator entity) {
334+
335+
return UUID.randomUUID().toString();
336+
}
337+
338+
@Override
339+
public boolean regenerate(TEntityWithGenerator entity) {
340+
341+
return true;
342+
}
343+
}
251344
}

0 commit comments

Comments
 (0)