Skip to content

Commit c54fc3b

Browse files
committed
refactor attachment storage logic
1 parent 6caa15a commit c54fc3b

14 files changed

Lines changed: 212 additions & 50 deletions

File tree

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package it.aboutbits.springboot.emailservice;
22

3+
import it.aboutbits.springboot.emailservice.lib.AttachmentCleanerCallback;
34
import it.aboutbits.springboot.emailservice.lib.AttachmentDataSource;
45
import it.aboutbits.springboot.emailservice.lib.EmailSchedulerCallback;
6+
import it.aboutbits.springboot.emailservice.lib.application.CleanupAttachmentFiles;
57
import it.aboutbits.springboot.emailservice.lib.application.EmailAttachmentMapper;
68
import it.aboutbits.springboot.emailservice.lib.application.EmailAttachmentMapperImpl;
79
import it.aboutbits.springboot.emailservice.lib.application.EmailMapper;
@@ -24,9 +26,9 @@
2426

2527
@AutoConfigurationPackage
2628
public class EmailServiceConfiguration {
27-
@Bean(initMethod = "init")
29+
@Bean(initMethod = "migrate")
2830
@ConditionalOnProperty(value = "aboutbits.emailservice.migrations.enabled", matchIfMissing = true)
29-
public EmailServiceMigrator springLiquibase(JdbcTemplate jdbcTemplate) {
31+
public EmailServiceMigrator emailServiceMigrator(JdbcTemplate jdbcTemplate) {
3032
return new EmailServiceMigrator(jdbcTemplate);
3133
}
3234

@@ -56,6 +58,12 @@ public SendScheduledEmails sendScheduledEmails(QueryEmail queryEmail, ManageEmai
5658
return new SendScheduledEmails(queryEmail, manageEmail, callbacks);
5759
}
5860

61+
@Bean
62+
@ConditionalOnProperty(value = "aboutbits.emailservice.scheduling.cleanup.enabled", matchIfMissing = true)
63+
public CleanupAttachmentFiles cleanupAttachments(QueryEmail queryEmail, ManageEmail manageEmail, List<AttachmentCleanerCallback> callbacks) {
64+
return new CleanupAttachmentFiles(queryEmail, manageEmail, callbacks);
65+
}
66+
5967
@Bean
6068
@ConditionalOnMissingBean(AttachmentDataSource.class)
6169
public AttachmentDataSource attachmentDataSource() {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package it.aboutbits.springboot.emailservice.lib;
2+
3+
public interface AttachmentCleanerCallback {
4+
void report(Report report);
5+
6+
record Report(
7+
int total,
8+
int cleaned,
9+
int errors
10+
) {
11+
}
12+
}

src/main/java/it/aboutbits/springboot/emailservice/lib/AttachmentDataSource.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,7 @@
77
public interface AttachmentDataSource {
88
InputStream getAttachmentPayload(AttachmentReference reference) throws AttachmentException;
99

10+
AttachmentReference storeAttachmentPayload(InputStream payload) throws AttachmentException;
11+
1012
void releaseAttachment(AttachmentReference reference) throws AttachmentException;
1113
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package it.aboutbits.springboot.emailservice.lib.application;
2+
3+
4+
import it.aboutbits.springboot.emailservice.lib.AttachmentCleanerCallback;
5+
import it.aboutbits.springboot.emailservice.lib.exception.AttachmentException;
6+
import lombok.AllArgsConstructor;
7+
import lombok.extern.log4j.Log4j2;
8+
import org.springframework.scheduling.annotation.Scheduled;
9+
10+
import java.util.List;
11+
12+
@AllArgsConstructor
13+
@Log4j2
14+
public class CleanupAttachmentFiles {
15+
private static final String JOB_DESCRIPTION = "Cleanup attachments of sent Emails.";
16+
17+
private final QueryEmail queryEmail;
18+
private final ManageEmail manageEmail;
19+
private final List<AttachmentCleanerCallback> callbacks;
20+
21+
@Scheduled(initialDelayString = "${aboutbits.emailservice.scheduling.interval:30000}", fixedDelayString = "${aboutbits.emailservice.scheduling.interval:30000}")
22+
void cleanupAttachments() {
23+
log.info("Start: " + JOB_DESCRIPTION);
24+
25+
var emailsToCleanup = queryEmail.readyToCleanup();
26+
27+
var countCleaned = 0;
28+
var countError = 0;
29+
for (var email : emailsToCleanup) {
30+
try {
31+
manageEmail.cleanupAttachments(email);
32+
countCleaned++;
33+
} catch (AttachmentException e) {
34+
countError++;
35+
}
36+
}
37+
38+
log.info("Finished: " + JOB_DESCRIPTION);
39+
log.info("Cleaned: {}, Errors: {}", countCleaned, countError);
40+
41+
for (var callback : callbacks) {
42+
callback.report(new AttachmentCleanerCallback.Report(
43+
emailsToCleanup.size(),
44+
countCleaned,
45+
countError
46+
));
47+
}
48+
}
49+
50+
51+
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
package it.aboutbits.springboot.emailservice.lib.application;
22

3-
import it.aboutbits.springboot.emailservice.lib.AttachmentReference;
43
import jakarta.validation.constraints.NotBlank;
54
import jakarta.validation.constraints.NotEmpty;
65
import lombok.Builder;
76
import lombok.NonNull;
87
import lombok.Singular;
98

9+
import java.io.InputStream;
1010
import java.time.OffsetDateTime;
1111
import java.util.List;
1212
import java.util.Set;
@@ -47,7 +47,7 @@ public record Email(
4747
@Builder
4848
public record Attachment(
4949
@NonNull
50-
AttachmentReference reference,
50+
InputStream payload,
5151
@NonNull
5252
@NotBlank
5353
String fileName,

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ void migrate() {
2828
text_body text not null,
2929
html_body text not null,
3030
attachments jsonb default '[]'::jsonb,
31-
scheduled_at timestamp with time zone default now() not null,
31+
attachments_cleaned bool default false not null,
32+
scheduled_at timestamp with time zone default now() not null,
3233
sent_at timestamp with time zone,
3334
error_at timestamp with time zone,
3435
error_message text,
@@ -41,8 +42,11 @@ updated_at timestamp with time zone default now() not null,
4142
create index if not exists email_service_emails_state_index
4243
on email_service_emails (state);
4344
44-
create index if not exists email_service_emails_sending_scheduled_at_index
45+
create index if not exists email_service_emails_scheduled_at_index
4546
on email_service_emails (scheduled_at);
47+
48+
create index if not exists email_service_emails_attachments_cleaned_index
49+
on email_service_emails (attachments_cleaned);
4650
4751
create table if not exists email_service_email_attachments
4852
(

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

Lines changed: 63 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,28 @@
66
import it.aboutbits.springboot.emailservice.lib.EmailDto;
77
import it.aboutbits.springboot.emailservice.lib.EmailState;
88
import it.aboutbits.springboot.emailservice.lib.exception.AttachmentException;
9+
import it.aboutbits.springboot.emailservice.lib.exception.EmailException;
910
import it.aboutbits.springboot.emailservice.lib.jpa.EmailRepository;
1011
import it.aboutbits.springboot.emailservice.lib.model.Email;
1112
import it.aboutbits.springboot.emailservice.lib.model.EmailAttachment;
1213
import jakarta.mail.MessagingException;
1314
import jakarta.validation.Valid;
1415
import lombok.NonNull;
15-
import lombok.extern.log4j.Log4j2;
16+
import lombok.extern.slf4j.Slf4j;
1617
import org.springframework.core.io.ByteArrayResource;
1718
import org.springframework.mail.javamail.JavaMailSender;
1819
import org.springframework.mail.javamail.MimeMessageHelper;
1920
import org.springframework.validation.annotation.Validated;
2021

2122
import java.io.IOException;
2223
import java.time.OffsetDateTime;
24+
import java.util.HashSet;
2325
import java.util.List;
2426
import java.util.Set;
25-
import java.util.stream.Collectors;
2627

2728

2829
@Validated
29-
@Log4j2
30+
@Slf4j
3031
public class ManageEmail {
3132
private final EmailRepository emailRepository;
3233
private final JavaMailSender mailSender;
@@ -45,35 +46,36 @@ public ManageEmail(
4546
this.emailMapper = emailMapper;
4647
}
4748

48-
public EmailDto schedule(@NonNull @Valid EmailParameter parameter) {
49-
var emailData = parameter.email();
50-
51-
final var email = new Email();
52-
email.setState(EmailState.PENDING);
53-
email.setScheduledAt(parameter.scheduledAt());
54-
email.setSubject(emailData.subject());
55-
email.setTextBody(emailData.textBody());
56-
email.setHtmlBody(emailData.htmlBody());
57-
email.setRecipients(emailData.recipients());
58-
email.setFromAddress(emailData.fromAddress());
59-
email.setFromName(emailData.fromName());
60-
email.setAttachments(emailData.attachments().stream()
61-
.map(a -> {
62-
var attachment = new EmailAttachment();
63-
attachment.setEmail(email);
64-
attachment.setReference(a.reference().value());
65-
attachment.setContentType(a.contentType());
66-
attachment.setFileName(a.fileName());
67-
return attachment;
68-
})
69-
.collect(Collectors.toSet())
70-
);
49+
public EmailDto schedule(@NonNull @Valid EmailParameter parameter) throws EmailException {
50+
Email email;
51+
try {
52+
email = fromParameter(parameter);
53+
} catch (AttachmentException e) {
54+
throw new EmailException(e);
55+
}
7156

7257
var savedEmail = emailRepository.save(email);
7358

7459
return emailMapper.toDto(savedEmail);
7560
}
7661

62+
public EmailDto sendOrFail(@NonNull @Valid EmailParameter parameter) throws EmailException {
63+
Email email;
64+
try {
65+
email = fromParameter(parameter);
66+
} catch (AttachmentException e) {
67+
throw new EmailException(e);
68+
}
69+
70+
var savedEmail = send(email);
71+
72+
if (savedEmail.hasFailed()) {
73+
throw new EmailException(savedEmail.getErrorMessage());
74+
}
75+
76+
return emailMapper.toDto(savedEmail);
77+
}
78+
7779
Email send(Email email) {
7880
if (EmailState.SENT.equals(email.getState())) {
7981
return email;
@@ -85,30 +87,52 @@ Email send(Email email) {
8587
email.setErrorMessage("");
8688
email.setSentAt(OffsetDateTime.now());
8789
} catch (MessagingException | AttachmentException | IOException e) {
88-
log.error("Failed to send email", e);
90+
log.error("Failed to send email: " + email.getId(), e);
8991
email.setErrorMessage(e.getMessage());
9092
email.setState(EmailState.ERROR);
9193
}
9294

93-
var updatedEmail = emailRepository.save(email);
94-
95-
if (updatedEmail.isSent()) {
96-
try {
97-
cleanupAttachments(email);
98-
} catch (AttachmentException e) {
99-
throw new IllegalStateException(e);
100-
}
101-
}
102-
103-
return updatedEmail;
95+
return emailRepository.save(email);
10496
}
10597

106-
private void cleanupAttachments(final Email email) throws AttachmentException {
98+
void cleanupAttachments(final Email email) throws AttachmentException {
10799
for (var attachment : email.getAttachments()) {
108100
attachmentDataSource.releaseAttachment(new AttachmentReference(attachment.getReference()));
109101
}
102+
email.setAttachmentsCleaned(true);
103+
emailRepository.save(email);
110104
}
111105

106+
private Email fromParameter(EmailParameter parameter) throws AttachmentException {
107+
var emailData = parameter.email();
108+
109+
final var email = new Email();
110+
email.setState(EmailState.PENDING);
111+
email.setScheduledAt(parameter.scheduledAt());
112+
email.setSubject(emailData.subject());
113+
email.setTextBody(emailData.textBody());
114+
email.setHtmlBody(emailData.htmlBody());
115+
email.setRecipients(emailData.recipients());
116+
email.setFromAddress(emailData.fromAddress());
117+
email.setFromName(emailData.fromName());
118+
119+
var attachments = new HashSet<EmailAttachment>();
120+
for (var attachment : parameter.email().attachments()) {
121+
var reference = attachmentDataSource.storeAttachmentPayload(attachment.payload());
122+
123+
var emailAttachment = new EmailAttachment();
124+
emailAttachment.setEmail(email);
125+
emailAttachment.setReference(reference.value());
126+
emailAttachment.setContentType(attachment.contentType());
127+
emailAttachment.setFileName(attachment.fileName());
128+
129+
attachments.add(emailAttachment);
130+
}
131+
132+
email.setAttachments(attachments);
133+
134+
return email;
135+
}
112136

113137
private void sendMail(Email notification) throws MessagingException, IOException, AttachmentException {
114138
sendMail(

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public List<EmailDto> byIds(Collection<Long> ids) {
3939
List<Email> readyToSend() {
4040
var entityGraph = entityManager.getEntityGraph("email_service_emails-entity-graph");
4141
return entityManager.createQuery("""
42-
SELECT e from Email e WHERE e.sendingScheduledAt < :scheduledBefore AND e.state IN (
42+
SELECT e from Email e WHERE e.scheduledAt < :scheduledBefore AND e.state IN (
4343
it.aboutbits.springboot.emailservice.lib.EmailState.PENDING,
4444
it.aboutbits.springboot.emailservice.lib.EmailState.ERROR
4545
)
@@ -49,6 +49,15 @@ List<Email> readyToSend() {
4949
.getResultList();
5050
}
5151

52+
List<Email> readyToCleanup() {
53+
var entityGraph = entityManager.getEntityGraph("email_service_emails-entity-graph");
54+
return entityManager.createQuery("""
55+
SELECT e from Email e WHERE e.attachmentsCleaned=false AND e.state=it.aboutbits.springboot.emailservice.lib.EmailState.SENT
56+
""", Email.class)
57+
.setHint("jakarta.persistence.fetchgraph", entityGraph)
58+
.getResultList();
59+
}
60+
5261
public Optional<EmailDto> byId(long id) {
5362
return emailRepository.findById(id).map(emailMapper::toDto);
5463
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public class SendScheduledEmails {
1818
private final List<EmailSchedulerCallback> callbacks;
1919

2020
@Scheduled(initialDelayString = "${aboutbits.emailservice.scheduling.interval:30000}", fixedDelayString = "${aboutbits.emailservice.scheduling.interval:30000}")
21-
void sendEmailNotifications() {
21+
void sendEmails() {
2222
log.info("Start: " + JOB_DESCRIPTION);
2323

2424
var emailsToSend = queryEmail.readyToSend();

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ public InputStream getAttachmentPayload(AttachmentReference reference) throws At
1212
throw new AttachmentException("attachments not available");
1313
}
1414

15+
@Override
16+
public AttachmentReference storeAttachmentPayload(InputStream payload) throws AttachmentException {
17+
throw new AttachmentException("attachments not available");
18+
}
19+
1520
@Override
1621
public void releaseAttachment(AttachmentReference reference) throws AttachmentException {
1722
throw new AttachmentException("attachments not available");

0 commit comments

Comments
 (0)