Skip to content

Commit ad00290

Browse files
committed
add new default AttachmentDataSource and add migrate it if necessary
1 parent 5631805 commit ad00290

4 files changed

Lines changed: 203 additions & 28 deletions

File tree

src/main/java/it/aboutbits/springboot/emailservice/EmailServiceConfiguration.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99
import it.aboutbits.springboot.emailservice.lib.application.EmailMapper;
1010
import it.aboutbits.springboot.emailservice.lib.application.EmailMapperImpl;
1111
import it.aboutbits.springboot.emailservice.lib.application.EmailServiceMigrator;
12+
import it.aboutbits.springboot.emailservice.lib.application.JdbcAttachmentDataSource;
1213
import it.aboutbits.springboot.emailservice.lib.application.ManageEmail;
1314
import it.aboutbits.springboot.emailservice.lib.application.QueryEmail;
1415
import it.aboutbits.springboot.emailservice.lib.application.SendScheduledEmails;
15-
import it.aboutbits.springboot.emailservice.lib.application.UnavailableAttachmentDataSource;
1616
import it.aboutbits.springboot.emailservice.lib.jpa.EmailRepository;
1717
import org.jspecify.annotations.NullMarked;
1818
import org.springframework.beans.factory.annotation.Value;
@@ -96,7 +96,14 @@ public CleanupAttachmentFiles cleanupAttachments(
9696

9797
@Bean
9898
@ConditionalOnMissingBean(AttachmentDataSource.class)
99-
public AttachmentDataSource attachmentDataSource() {
100-
return new UnavailableAttachmentDataSource();
99+
public JdbcAttachmentDataSource attachmentDataSource(
100+
JdbcTemplate jdbcTemplate,
101+
@Value("${aboutbits.emailservice.migrations.enabled:true}") boolean migrationsEnabled
102+
) {
103+
var dataSource = new JdbcAttachmentDataSource(jdbcTemplate);
104+
if (migrationsEnabled) {
105+
dataSource.migrate();
106+
}
107+
return dataSource;
101108
}
102109
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package it.aboutbits.springboot.emailservice.lib.application;
2+
3+
import it.aboutbits.springboot.emailservice.lib.AttachmentDataSource;
4+
import it.aboutbits.springboot.emailservice.lib.exception.AttachmentException;
5+
import lombok.extern.slf4j.Slf4j;
6+
import org.jspecify.annotations.NullMarked;
7+
import org.springframework.dao.DataAccessException;
8+
import org.springframework.dao.EmptyResultDataAccessException;
9+
import org.springframework.jdbc.core.JdbcTemplate;
10+
11+
import java.io.ByteArrayInputStream;
12+
import java.io.IOException;
13+
import java.io.InputStream;
14+
15+
@Slf4j
16+
@NullMarked
17+
public class JdbcAttachmentDataSource implements AttachmentDataSource {
18+
private final JdbcTemplate jdbcTemplate;
19+
20+
public JdbcAttachmentDataSource(JdbcTemplate jdbcTemplate) {
21+
this.jdbcTemplate = jdbcTemplate;
22+
}
23+
24+
public void migrate() {
25+
log.info("EmailService: running attachment payload DB migrations...");
26+
27+
jdbcTemplate.execute(
28+
//@formatter:off
29+
"""
30+
31+
create table if not exists email_service_attachment_payloads
32+
(
33+
id bigint generated by default as identity
34+
primary key,
35+
payload bytea not null,
36+
created_at timestamp with time zone default now() not null
37+
);
38+
"""
39+
//@formatter:on
40+
);
41+
42+
log.info("EmailService: attachment payload migrations done!");
43+
}
44+
45+
@Override
46+
public InputStream getAttachmentPayload(long fileReference) throws AttachmentException {
47+
try {
48+
var payload = jdbcTemplate.queryForObject(
49+
"select payload from email_service_attachment_payloads where id = ?",
50+
byte[].class,
51+
fileReference
52+
);
53+
if (payload == null) {
54+
throw new AttachmentException("attachment payload not found: " + fileReference);
55+
}
56+
return new ByteArrayInputStream(payload);
57+
} catch (EmptyResultDataAccessException e) {
58+
throw new AttachmentException("attachment payload not found: " + fileReference, e);
59+
} catch (DataAccessException e) {
60+
throw new AttachmentException("failed to load attachment payload: " + fileReference, e);
61+
}
62+
}
63+
64+
@Override
65+
public long storeAttachmentPayload(InputStream payload) throws AttachmentException {
66+
final byte[] bytes;
67+
try (payload) {
68+
bytes = payload.readAllBytes();
69+
} catch (IOException e) {
70+
throw new AttachmentException("failed to read attachment payload", e);
71+
}
72+
73+
try {
74+
var id = jdbcTemplate.queryForObject(
75+
"insert into email_service_attachment_payloads (payload) values (?) returning id",
76+
Long.class,
77+
(Object) bytes
78+
);
79+
if (id == null) {
80+
throw new AttachmentException("failed to store attachment payload");
81+
}
82+
return id;
83+
} catch (DataAccessException e) {
84+
throw new AttachmentException("failed to store attachment payload", e);
85+
}
86+
}
87+
88+
@Override
89+
public void releaseAttachment(long fileReference) throws AttachmentException {
90+
try {
91+
jdbcTemplate.update(
92+
"delete from email_service_attachment_payloads where id = ?",
93+
fileReference
94+
);
95+
} catch (DataAccessException e) {
96+
throw new AttachmentException("failed to release attachment payload: " + fileReference, e);
97+
}
98+
}
99+
}

src/main/java/it/aboutbits/springboot/emailservice/lib/application/UnavailableAttachmentDataSource.java

Lines changed: 0 additions & 25 deletions
This file was deleted.
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package it.aboutbits.springboot.emailservice.lib.application;
2+
3+
import it.aboutbits.springboot.emailservice.lib.AttachmentDataSource;
4+
import it.aboutbits.springboot.emailservice.lib.exception.AttachmentException;
5+
import it.aboutbits.springboot.emailservice.support.database.WithPostgres;
6+
import org.jspecify.annotations.NullMarked;
7+
import org.junit.jupiter.api.Test;
8+
import org.springframework.beans.factory.annotation.Autowired;
9+
import org.springframework.boot.test.context.SpringBootTest;
10+
11+
import java.io.ByteArrayInputStream;
12+
13+
import static org.assertj.core.api.Assertions.assertThat;
14+
import static org.assertj.core.api.Assertions.assertThatCode;
15+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
16+
17+
@SpringBootTest
18+
@WithPostgres
19+
@NullMarked
20+
class JdbcAttachmentDataSourceTest {
21+
@Autowired
22+
AttachmentDataSource attachmentDataSource;
23+
24+
@Test
25+
void defaultAttachmentDataSource_shouldBeJdbcBased() {
26+
assertThat(attachmentDataSource).isInstanceOf(JdbcAttachmentDataSource.class);
27+
}
28+
29+
@Test
30+
void givenPayload_store_shouldBeReadableAgain() throws Exception {
31+
var payload = new byte[]{1, 2, 3, 4, 5};
32+
33+
var fileReference = attachmentDataSource.storeAttachmentPayload(new ByteArrayInputStream(payload));
34+
35+
try (var stored = attachmentDataSource.getAttachmentPayload(fileReference)) {
36+
assertThat(stored.readAllBytes()).isEqualTo(payload);
37+
}
38+
}
39+
40+
@Test
41+
void givenPayload_store_shouldClosePayloadStream() throws Exception {
42+
var payload = new TrackingInputStream(new byte[]{1, 2, 3});
43+
44+
attachmentDataSource.storeAttachmentPayload(payload);
45+
46+
assertThat(payload.closed).isTrue();
47+
}
48+
49+
@Test
50+
void givenMultiplePayloads_store_shouldReturnDistinctReferences() throws Exception {
51+
var first = attachmentDataSource.storeAttachmentPayload(new ByteArrayInputStream(new byte[]{1}));
52+
var second = attachmentDataSource.storeAttachmentPayload(new ByteArrayInputStream(new byte[]{2}));
53+
54+
assertThat(first).isNotEqualTo(second);
55+
}
56+
57+
@Test
58+
void givenUnknownReference_get_shouldFail() {
59+
assertThatExceptionOfType(AttachmentException.class).isThrownBy(
60+
() -> attachmentDataSource.getAttachmentPayload(1L)
61+
);
62+
}
63+
64+
@Test
65+
void givenStoredPayload_release_shouldRemoveIt() throws Exception {
66+
var fileReference = attachmentDataSource.storeAttachmentPayload(new ByteArrayInputStream(new byte[]{1, 2, 3}));
67+
68+
attachmentDataSource.releaseAttachment(fileReference);
69+
70+
assertThatExceptionOfType(AttachmentException.class).isThrownBy(
71+
() -> attachmentDataSource.getAttachmentPayload(fileReference)
72+
);
73+
}
74+
75+
@Test
76+
void givenUnknownReference_release_shouldBeIdempotent() {
77+
assertThatCode(
78+
() -> attachmentDataSource.releaseAttachment(1L)
79+
).doesNotThrowAnyException();
80+
}
81+
82+
private static final class TrackingInputStream extends ByteArrayInputStream {
83+
private boolean closed = false;
84+
85+
private TrackingInputStream(byte[] buf) {
86+
super(buf);
87+
}
88+
89+
@Override
90+
public void close() {
91+
closed = true;
92+
}
93+
}
94+
}

0 commit comments

Comments
 (0)