Skip to content

Commit e744a26

Browse files
authored
Merge pull request #402 from qbicsoftware/release/2023-10-17
Release 2023 10 17
2 parents 35e4502 + 70143d9 commit e744a26

File tree

32 files changed

+987
-62
lines changed

32 files changed

+987
-62
lines changed

database-connector/src/main/java/life/qbic/projectmanagement/experiment/persistence/BatchJpaRepository.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import life.qbic.projectmanagement.domain.project.repository.BatchRepository;
99
import life.qbic.projectmanagement.domain.project.sample.Batch;
1010
import life.qbic.projectmanagement.domain.project.sample.BatchId;
11-
import life.qbic.projectmanagement.domain.project.service.BatchDomainService.ResponseCode;
11+
import life.qbic.projectmanagement.application.batch.BatchRegistrationService.ResponseCode;
1212
import org.springframework.beans.factory.annotation.Autowired;
1313
import org.springframework.stereotype.Repository;
1414

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package life.qbic.domain.concepts.communication;
2+
3+
/**
4+
* <b>MailAttachment</b>
5+
*
6+
* <p>Encapsulates information about an attachment, e.g. a file added to an email</p>
7+
*
8+
* @since 1.0.0
9+
*/
10+
public record Attachment(String content, String name) {
11+
12+
}

domain-concept/src/main/java/life/qbic/domain/concepts/communication/CommunicationService.java

+16
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,22 @@
99
*/
1010
public interface CommunicationService {
1111

12+
/**
13+
* Sends a message (e.g. email) to a recipient
14+
* @param subject The subject of the message
15+
* @param recipient The Recipient of the message
16+
* @param content The Content of the message
17+
* @throws CommunicationException
18+
*/
1219
void send(Subject subject, Recipient recipient, Content content) throws CommunicationException;
1320

21+
/**
22+
* Sends a message (e.g. email) with an attached file to a recipient
23+
* @param subject The subject of the message
24+
* @param recipient The Recipient of the message
25+
* @param content The Content of the message
26+
* @param attachment An Attachment object denoting name and content of the attached file
27+
* @throws CommunicationException
28+
*/
29+
void send(Subject subject, Recipient recipient, Content content, Attachment attachment) throws CommunicationException;
1430
}

newshandler/src/main/java/life/qbic/newshandler/usermanagement/email/EmailCommunicationService.java

+59-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
package life.qbic.newshandler.usermanagement.email;
22

3+
import jakarta.activation.DataHandler;
4+
import jakarta.activation.DataSource;
5+
import jakarta.mail.BodyPart;
36
import jakarta.mail.Message.RecipientType;
47
import jakarta.mail.MessagingException;
8+
import jakarta.mail.Multipart;
59
import jakarta.mail.Transport;
610
import jakarta.mail.internet.InternetAddress;
11+
import jakarta.mail.internet.MimeBodyPart;
712
import jakarta.mail.internet.MimeMessage;
13+
import jakarta.mail.internet.MimeMultipart;
14+
import jakarta.mail.util.ByteArrayDataSource;
15+
import java.io.UnsupportedEncodingException;
816
import java.util.Objects;
17+
import life.qbic.domain.concepts.communication.Attachment;
918
import life.qbic.domain.concepts.communication.CommunicationException;
1019
import life.qbic.domain.concepts.communication.CommunicationService;
1120
import life.qbic.domain.concepts.communication.Content;
@@ -27,7 +36,7 @@ public class EmailCommunicationService implements CommunicationService {
2736
private static final Logger log = LoggerFactory.logger(
2837
EmailCommunicationService.class);
2938
private static final String NO_REPLY_ADDRESS = "[email protected]";
30-
39+
private static final String NOTIFICATION_FAILED = "Notification of recipient failed!";
3140
private static final String SIGNATURE = """
3241
3342
With kind regards,
@@ -54,17 +63,63 @@ public void send(Subject subject, Recipient recipient, Content content) {
5463
"Sending email with subject %s to %s".formatted(subject.content(), recipient.address()));
5564
} catch (MessagingException e) {
5665
log.error("Could not send email to " + recipient.address(), e);
57-
throw new CommunicationException("Notification of recipient failed!");
66+
throw new CommunicationException(NOTIFICATION_FAILED);
5867
}
5968
}
6069

61-
private MimeMessage setupMessage(Subject subject, Recipient recipient, Content content)
70+
@Override
71+
public void send(Subject subject, Recipient recipient, Content content, Attachment attachment) {
72+
try {
73+
var message = setupMessageWithAttachment(subject, recipient, content, attachment);
74+
Transport.send(message);
75+
log.debug(
76+
"Sending email with subject %s to %s".formatted(subject.content(), recipient.address()));
77+
} catch (MessagingException e) {
78+
log.error("Could not send email to " + recipient.address(), e);
79+
throw new CommunicationException(NOTIFICATION_FAILED);
80+
} catch (UnsupportedEncodingException e) {
81+
log.error("Could not create attachment for email to " + recipient.address(), e);
82+
throw new CommunicationException(NOTIFICATION_FAILED);
83+
}
84+
}
85+
86+
private MimeMessage setupMessageWithoutContent(Subject subject, Recipient recipient)
6287
throws MessagingException {
6388
var message = this.mailServerConfiguration.mimeMessage();
6489
message.setFrom(new InternetAddress(NO_REPLY_ADDRESS));
65-
message.setContent(combineMessageWithRegards(content).content(), "text/plain");
6690
message.setRecipient(RecipientType.TO, new InternetAddress(recipient.address()));
6791
message.setSubject(subject.content());
6892
return message;
6993
}
94+
95+
private MimeMessage setupMessage(Subject subject, Recipient recipient, Content content)
96+
throws MessagingException {
97+
var message = setupMessageWithoutContent(subject, recipient);
98+
message.setContent(combineMessageWithRegards(content).content(), "text/plain");
99+
return message;
100+
}
101+
102+
private MimeMessage setupMessageWithAttachment(Subject subject, Recipient recipient,
103+
Content content, Attachment attachment)
104+
throws MessagingException, UnsupportedEncodingException {
105+
106+
var message = setupMessageWithoutContent(subject, recipient);
107+
108+
BodyPart messageBodyPart = new MimeBodyPart();
109+
messageBodyPart.setContent(combineMessageWithRegards(content).content(), "text/plain");
110+
111+
Multipart multipart = new MimeMultipart();
112+
multipart.addBodyPart(messageBodyPart);
113+
114+
BodyPart attachmentPart = new MimeBodyPart();
115+
DataSource dataSource = new ByteArrayDataSource(attachment.content().getBytes("UTF-8"),
116+
"application/octet-stream");
117+
attachmentPart.setDataHandler(new DataHandler(dataSource));
118+
attachmentPart.setFileName(attachment.name());
119+
multipart.addBodyPart(attachmentPart);
120+
121+
message.setContent(multipart);
122+
return message;
123+
}
124+
70125
}

projectmanagement-domain/src/main/java/life/qbic/projectmanagement/application/AppContextProvider.java

+9-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ public interface AppContextProvider {
1616
* @return a fully resolvable URL
1717
* @since 1.0.0
1818
*/
19-
String urlToProject(String projectId);
2019

20+
String urlToProject(String projectId);
21+
/**
22+
* Returns a resolvable URL to the target project's sample page resource in the application.
23+
*
24+
* @param projectId the project id
25+
* @return a fully resolvable URL
26+
* @since 1.0.0
27+
*/
28+
String urlToSamplePage(String projectId);
2129
}

projectmanagement-domain/src/main/java/life/qbic/projectmanagement/application/Messages.java

+33-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,38 @@ private Messages() {
1313

1414
}
1515

16+
/**
17+
* A pre-formatted message that informs a user about newly created samples and their identifiers
18+
* in the data manager.
19+
*
20+
* @param fullNameUser the name of the user to inform for addressing them politely
21+
* @param projectTitle the title of the project, will be in the message to inform the user about
22+
* which project they have been granted access with
23+
* @param batchName the name of the batch that was added
24+
* @param sampleUri a uniform resource identifier of the sample page of this project, that the
25+
* recipient can use to access the newly registered samples
26+
* @return the filled out template message
27+
* @since 1.0.0
28+
*/
29+
public static String samplesAddedToProject(String fullNameUser, String projectTitle,
30+
String batchName, String sampleUri) {
31+
return String.format("""
32+
Dear %s,
33+
34+
the new batch ('%s') of samples has been added to the project:
35+
36+
'%s'
37+
38+
Sample information and QBiC identifiers have been added to the Data Manager.
39+
These identifiers uniquely characterize each added sample. They will be used to attach data
40+
for each of the samples, as soon as it has been measured and uploaded.
41+
42+
Please click the link below to access the sample information after login:
43+
44+
%s
45+
""", fullNameUser, batchName, projectTitle, sampleUri);
46+
}
47+
1648
/**
1749
* A pre-formatted message that informs a user about their new access grant to a project in the
1850
* data manager.
@@ -24,7 +56,7 @@ private Messages() {
2456
* access the project
2557
* @return the filled out template message
2658
* @since 1.0.0
27-
*/
59+
*/
2860
public static String projectAccessToUser(String fullNameUser, String projectTitle,
2961
String projectUri) {
3062
return String.format("""

projectmanagement-domain/src/main/java/life/qbic/projectmanagement/application/authorization/acl/ProjectAccessService.java

+8
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ public interface ProjectAccessService {
2121
*/
2222
List<String> listUsers(ProjectId projectId);
2323

24+
/**
25+
* Lists all active users which have a permission within the specific project
26+
*
27+
* @param projectId the identifier of the project
28+
* @return a list of user ids of active users that are associated with the project
29+
*/
30+
List<String> listActiveUsers(ProjectId projectId);
31+
2432
/**
2533
* Lists all users which have a permission on the project
2634
*

projectmanagement-domain/src/main/java/life/qbic/projectmanagement/application/authorization/acl/ProjectAccessServiceImpl.java

+14-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static java.util.Objects.requireNonNull;
44

5+
import java.util.ArrayList;
56
import java.util.List;
67
import java.util.function.Predicate;
78
import life.qbic.projectmanagement.application.authorization.QbicUserDetails;
@@ -19,6 +20,7 @@
1920
import org.springframework.security.acls.model.Permission;
2021
import org.springframework.security.acls.model.Sid;
2122
import org.springframework.security.core.GrantedAuthority;
23+
import org.springframework.security.core.userdetails.UserDetails;
2224
import org.springframework.security.core.userdetails.UserDetailsService;
2325
import org.springframework.stereotype.Service;
2426
import org.springframework.transaction.annotation.Transactional;
@@ -43,7 +45,18 @@ public List<String> listUsers(ProjectId projectId) {
4345
.filter(it -> it instanceof QbicUserDetails)
4446
.map(it -> (QbicUserDetails) it)
4547
.map(QbicUserDetails::getUserId)
46-
.toList();
48+
.distinct().toList();
49+
}
50+
51+
@Transactional
52+
@Override
53+
public List<String> listActiveUsers(ProjectId projectId) {
54+
return listUsernames(projectId).stream().map(userDetailsService::loadUserByUsername)
55+
.filter(it -> it instanceof QbicUserDetails)
56+
.map(it -> (QbicUserDetails) it)
57+
.filter(QbicUserDetails::isEnabled)
58+
.map(QbicUserDetails::getUserId)
59+
.distinct().toList();
4760
}
4861

4962
@Transactional
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,66 @@
11
package life.qbic.projectmanagement.application.batch;
22

3+
import static org.slf4j.LoggerFactory.getLogger;
4+
5+
import java.util.Objects;
36
import life.qbic.application.commons.Result;
7+
import life.qbic.projectmanagement.application.ProjectInformationService;
8+
import life.qbic.projectmanagement.domain.project.ProjectId;
49
import life.qbic.projectmanagement.domain.project.repository.BatchRepository;
510
import life.qbic.projectmanagement.domain.project.sample.Batch;
611
import life.qbic.projectmanagement.domain.project.sample.BatchId;
712
import life.qbic.projectmanagement.domain.project.sample.SampleId;
13+
import life.qbic.projectmanagement.domain.project.service.BatchDomainService;
14+
import org.slf4j.Logger;
815
import org.springframework.beans.factory.annotation.Autowired;
916
import org.springframework.stereotype.Service;
1017

1118
/**
12-
* <b><class short description - 1 Line!></b>
13-
*
14-
* <p><More detailed description - When to use, what it solves, etc.></p>
19+
* <b>Batch Registration Service</b>
20+
* <p>
21+
* Service that handles {@link Batch} creation and deletion events, that need to dispatch domain
22+
* events.
1523
*
16-
* @since <version tag>
24+
* @since 1.0.0
1725
*/
1826
@Service
1927
public class BatchRegistrationService {
2028

2129
private final BatchRepository batchRepository;
30+
private final BatchDomainService batchDomainService;
31+
private final ProjectInformationService projectInformationService;
32+
private static final Logger log = getLogger(BatchRegistrationService.class);
2233

23-
public BatchRegistrationService(@Autowired BatchRepository batchRepository) {
24-
this.batchRepository = batchRepository;
34+
@Autowired
35+
public BatchRegistrationService(BatchRepository batchRepository,
36+
BatchDomainService batchDomainService, ProjectInformationService projectInformationService) {
37+
this.batchRepository = Objects.requireNonNull(batchRepository);
38+
this.batchDomainService = Objects.requireNonNull(batchDomainService);
39+
this.projectInformationService = Objects.requireNonNull(projectInformationService);
2540
}
2641

27-
public Result<BatchId, ResponseCode> registerBatch(String label, boolean isPilot) {
28-
Batch batch = Batch.create(label, isPilot);
29-
var result = batchRepository.add(batch);
30-
if (result.isError()) {
42+
/**
43+
* Registers a new batch of samples that serves as reference for sample processing in the lab for
44+
* measurement and analysis purposes.
45+
*
46+
* @param label a human-readable semantic descriptor of the batch
47+
* @param isPilot a flag that indicates the batch to describe as pilot submission batch. Pilots
48+
* are usually followed by a complete batch that represents the measurements of the
49+
* complete experiment.
50+
* @param projectId id of the project this batch is added to
51+
* @return a result object with the response. If the registration failed, a response code will be
52+
* provided.
53+
* @since 1.0.0
54+
*/
55+
public Result<BatchId, ResponseCode> registerBatch(String label, boolean isPilot,
56+
ProjectId projectId) {
57+
var project = projectInformationService.find(projectId);
58+
if (project.isEmpty()) {
59+
log.error("Batch registration aborted. Reason: project with id:"+projectId+" was not found");
3160
return Result.fromError(ResponseCode.BATCH_CREATION_FAILED);
3261
}
33-
return Result.fromValue(batch.batchId());
62+
String projectTitle = project.get().getProjectIntent().projectTitle().title();
63+
return batchDomainService.register(label, isPilot, projectTitle, projectId);
3464
}
3565

3666
public Result<BatchId, ResponseCode> addSampleToBatch(SampleId sampleId, BatchId batchId) {
@@ -48,11 +78,12 @@ public Result<BatchId, ResponseCode> addSampleToBatch(SampleId sampleId, BatchId
4878
}
4979
}
5080

81+
5182
public enum ResponseCode {
5283
BATCH_UPDATE_FAILED,
5384
BATCH_NOT_FOUND,
54-
BATCH_CREATION_FAILED
55-
85+
BATCH_CREATION_FAILED,
86+
BATCH_REGISTRATION_FAILED
5687
}
5788

5889
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package life.qbic.projectmanagement.application.policy;
2+
3+
import life.qbic.domain.concepts.DomainEventDispatcher;
4+
import life.qbic.projectmanagement.application.policy.directive.InformUsersAboutBatchRegistration;
5+
6+
/**
7+
* <b>Policy: Batch Registered</b>
8+
* <p>
9+
* A collection of all directives that need to be executed after a new batch of
10+
* samples has been registered for measurement.
11+
* <p>
12+
* The policy subscribes to events of type
13+
* {@link life.qbic.projectmanagement.domain.project.sample.event.BatchRegistered} and ensures the
14+
* registration of all business required directives.
15+
*
16+
* @since 1.0.0
17+
*/
18+
public class BatchRegisteredPolicy {
19+
20+
private final InformUsersAboutBatchRegistration informUsers;
21+
22+
/**
23+
* Creates an instance of a {@link BatchRegisteredPolicy} object.
24+
* <p>
25+
* All directives will be created and subscribed upon instantiation.
26+
*
27+
* @param informUsers directive to inform users of a project about the new samples of a batch
28+
* {@link life.qbic.projectmanagement.domain.project.sample.Batch}
29+
* @since 1.0.0
30+
*/
31+
public BatchRegisteredPolicy(InformUsersAboutBatchRegistration informUsers) {
32+
this.informUsers = informUsers;
33+
DomainEventDispatcher.instance().subscribe(this.informUsers);
34+
}
35+
}

0 commit comments

Comments
 (0)