Skip to content

Commit

Permalink
Merge pull request #402 from qbicsoftware/release/2023-10-17
Browse files Browse the repository at this point in the history
Release 2023 10 17
  • Loading branch information
KochTobi authored Oct 17, 2023
2 parents 35e4502 + 70143d9 commit e744a26
Show file tree
Hide file tree
Showing 32 changed files with 987 additions and 62 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import life.qbic.projectmanagement.domain.project.repository.BatchRepository;
import life.qbic.projectmanagement.domain.project.sample.Batch;
import life.qbic.projectmanagement.domain.project.sample.BatchId;
import life.qbic.projectmanagement.domain.project.service.BatchDomainService.ResponseCode;
import life.qbic.projectmanagement.application.batch.BatchRegistrationService.ResponseCode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package life.qbic.domain.concepts.communication;

/**
* <b>MailAttachment</b>
*
* <p>Encapsulates information about an attachment, e.g. a file added to an email</p>
*
* @since 1.0.0
*/
public record Attachment(String content, String name) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,22 @@
*/
public interface CommunicationService {

/**
* Sends a message (e.g. email) to a recipient
* @param subject The subject of the message
* @param recipient The Recipient of the message
* @param content The Content of the message
* @throws CommunicationException
*/
void send(Subject subject, Recipient recipient, Content content) throws CommunicationException;

/**
* Sends a message (e.g. email) with an attached file to a recipient
* @param subject The subject of the message
* @param recipient The Recipient of the message
* @param content The Content of the message
* @param attachment An Attachment object denoting name and content of the attached file
* @throws CommunicationException
*/
void send(Subject subject, Recipient recipient, Content content, Attachment attachment) throws CommunicationException;
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
package life.qbic.newshandler.usermanagement.email;

import jakarta.activation.DataHandler;
import jakarta.activation.DataSource;
import jakarta.mail.BodyPart;
import jakarta.mail.Message.RecipientType;
import jakarta.mail.MessagingException;
import jakarta.mail.Multipart;
import jakarta.mail.Transport;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeBodyPart;
import jakarta.mail.internet.MimeMessage;
import jakarta.mail.internet.MimeMultipart;
import jakarta.mail.util.ByteArrayDataSource;
import java.io.UnsupportedEncodingException;
import java.util.Objects;
import life.qbic.domain.concepts.communication.Attachment;
import life.qbic.domain.concepts.communication.CommunicationException;
import life.qbic.domain.concepts.communication.CommunicationService;
import life.qbic.domain.concepts.communication.Content;
Expand All @@ -27,7 +36,7 @@ public class EmailCommunicationService implements CommunicationService {
private static final Logger log = LoggerFactory.logger(
EmailCommunicationService.class);
private static final String NO_REPLY_ADDRESS = "[email protected]";

private static final String NOTIFICATION_FAILED = "Notification of recipient failed!";
private static final String SIGNATURE = """
With kind regards,
Expand All @@ -54,17 +63,63 @@ public void send(Subject subject, Recipient recipient, Content content) {
"Sending email with subject %s to %s".formatted(subject.content(), recipient.address()));
} catch (MessagingException e) {
log.error("Could not send email to " + recipient.address(), e);
throw new CommunicationException("Notification of recipient failed!");
throw new CommunicationException(NOTIFICATION_FAILED);
}
}

private MimeMessage setupMessage(Subject subject, Recipient recipient, Content content)
@Override
public void send(Subject subject, Recipient recipient, Content content, Attachment attachment) {
try {
var message = setupMessageWithAttachment(subject, recipient, content, attachment);
Transport.send(message);
log.debug(
"Sending email with subject %s to %s".formatted(subject.content(), recipient.address()));
} catch (MessagingException e) {
log.error("Could not send email to " + recipient.address(), e);
throw new CommunicationException(NOTIFICATION_FAILED);
} catch (UnsupportedEncodingException e) {
log.error("Could not create attachment for email to " + recipient.address(), e);
throw new CommunicationException(NOTIFICATION_FAILED);
}
}

private MimeMessage setupMessageWithoutContent(Subject subject, Recipient recipient)
throws MessagingException {
var message = this.mailServerConfiguration.mimeMessage();
message.setFrom(new InternetAddress(NO_REPLY_ADDRESS));
message.setContent(combineMessageWithRegards(content).content(), "text/plain");
message.setRecipient(RecipientType.TO, new InternetAddress(recipient.address()));
message.setSubject(subject.content());
return message;
}

private MimeMessage setupMessage(Subject subject, Recipient recipient, Content content)
throws MessagingException {
var message = setupMessageWithoutContent(subject, recipient);
message.setContent(combineMessageWithRegards(content).content(), "text/plain");
return message;
}

private MimeMessage setupMessageWithAttachment(Subject subject, Recipient recipient,
Content content, Attachment attachment)
throws MessagingException, UnsupportedEncodingException {

var message = setupMessageWithoutContent(subject, recipient);

BodyPart messageBodyPart = new MimeBodyPart();
messageBodyPart.setContent(combineMessageWithRegards(content).content(), "text/plain");

Multipart multipart = new MimeMultipart();
multipart.addBodyPart(messageBodyPart);

BodyPart attachmentPart = new MimeBodyPart();
DataSource dataSource = new ByteArrayDataSource(attachment.content().getBytes("UTF-8"),
"application/octet-stream");
attachmentPart.setDataHandler(new DataHandler(dataSource));
attachmentPart.setFileName(attachment.name());
multipart.addBodyPart(attachmentPart);

message.setContent(multipart);
return message;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ public interface AppContextProvider {
* @return a fully resolvable URL
* @since 1.0.0
*/
String urlToProject(String projectId);

String urlToProject(String projectId);
/**
* Returns a resolvable URL to the target project's sample page resource in the application.
*
* @param projectId the project id
* @return a fully resolvable URL
* @since 1.0.0
*/
String urlToSamplePage(String projectId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,38 @@ private Messages() {

}

/**
* A pre-formatted message that informs a user about newly created samples and their identifiers
* in the data manager.
*
* @param fullNameUser the name of the user to inform for addressing them politely
* @param projectTitle the title of the project, will be in the message to inform the user about
* which project they have been granted access with
* @param batchName the name of the batch that was added
* @param sampleUri a uniform resource identifier of the sample page of this project, that the
* recipient can use to access the newly registered samples
* @return the filled out template message
* @since 1.0.0
*/
public static String samplesAddedToProject(String fullNameUser, String projectTitle,
String batchName, String sampleUri) {
return String.format("""
Dear %s,
the new batch ('%s') of samples has been added to the project:
'%s'
Sample information and QBiC identifiers have been added to the Data Manager.
These identifiers uniquely characterize each added sample. They will be used to attach data
for each of the samples, as soon as it has been measured and uploaded.
Please click the link below to access the sample information after login:
%s
""", fullNameUser, batchName, projectTitle, sampleUri);
}

/**
* A pre-formatted message that informs a user about their new access grant to a project in the
* data manager.
Expand All @@ -24,7 +56,7 @@ private Messages() {
* access the project
* @return the filled out template message
* @since 1.0.0
*/
*/
public static String projectAccessToUser(String fullNameUser, String projectTitle,
String projectUri) {
return String.format("""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ public interface ProjectAccessService {
*/
List<String> listUsers(ProjectId projectId);

/**
* Lists all active users which have a permission within the specific project
*
* @param projectId the identifier of the project
* @return a list of user ids of active users that are associated with the project
*/
List<String> listActiveUsers(ProjectId projectId);

/**
* Lists all users which have a permission on the project
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static java.util.Objects.requireNonNull;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
import life.qbic.projectmanagement.application.authorization.QbicUserDetails;
Expand All @@ -19,6 +20,7 @@
import org.springframework.security.acls.model.Permission;
import org.springframework.security.acls.model.Sid;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -43,7 +45,18 @@ public List<String> listUsers(ProjectId projectId) {
.filter(it -> it instanceof QbicUserDetails)
.map(it -> (QbicUserDetails) it)
.map(QbicUserDetails::getUserId)
.toList();
.distinct().toList();
}

@Transactional
@Override
public List<String> listActiveUsers(ProjectId projectId) {
return listUsernames(projectId).stream().map(userDetailsService::loadUserByUsername)
.filter(it -> it instanceof QbicUserDetails)
.map(it -> (QbicUserDetails) it)
.filter(QbicUserDetails::isEnabled)
.map(QbicUserDetails::getUserId)
.distinct().toList();
}

@Transactional
Expand Down
Original file line number Diff line number Diff line change
@@ -1,36 +1,66 @@
package life.qbic.projectmanagement.application.batch;

import static org.slf4j.LoggerFactory.getLogger;

import java.util.Objects;
import life.qbic.application.commons.Result;
import life.qbic.projectmanagement.application.ProjectInformationService;
import life.qbic.projectmanagement.domain.project.ProjectId;
import life.qbic.projectmanagement.domain.project.repository.BatchRepository;
import life.qbic.projectmanagement.domain.project.sample.Batch;
import life.qbic.projectmanagement.domain.project.sample.BatchId;
import life.qbic.projectmanagement.domain.project.sample.SampleId;
import life.qbic.projectmanagement.domain.project.service.BatchDomainService;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
* <b><class short description - 1 Line!></b>
*
* <p><More detailed description - When to use, what it solves, etc.></p>
* <b>Batch Registration Service</b>
* <p>
* Service that handles {@link Batch} creation and deletion events, that need to dispatch domain
* events.
*
* @since <version tag>
* @since 1.0.0
*/
@Service
public class BatchRegistrationService {

private final BatchRepository batchRepository;
private final BatchDomainService batchDomainService;
private final ProjectInformationService projectInformationService;
private static final Logger log = getLogger(BatchRegistrationService.class);

public BatchRegistrationService(@Autowired BatchRepository batchRepository) {
this.batchRepository = batchRepository;
@Autowired
public BatchRegistrationService(BatchRepository batchRepository,
BatchDomainService batchDomainService, ProjectInformationService projectInformationService) {
this.batchRepository = Objects.requireNonNull(batchRepository);
this.batchDomainService = Objects.requireNonNull(batchDomainService);
this.projectInformationService = Objects.requireNonNull(projectInformationService);
}

public Result<BatchId, ResponseCode> registerBatch(String label, boolean isPilot) {
Batch batch = Batch.create(label, isPilot);
var result = batchRepository.add(batch);
if (result.isError()) {
/**
* Registers a new batch of samples that serves as reference for sample processing in the lab for
* measurement and analysis purposes.
*
* @param label a human-readable semantic descriptor of the batch
* @param isPilot a flag that indicates the batch to describe as pilot submission batch. Pilots
* are usually followed by a complete batch that represents the measurements of the
* complete experiment.
* @param projectId id of the project this batch is added to
* @return a result object with the response. If the registration failed, a response code will be
* provided.
* @since 1.0.0
*/
public Result<BatchId, ResponseCode> registerBatch(String label, boolean isPilot,
ProjectId projectId) {
var project = projectInformationService.find(projectId);
if (project.isEmpty()) {
log.error("Batch registration aborted. Reason: project with id:"+projectId+" was not found");
return Result.fromError(ResponseCode.BATCH_CREATION_FAILED);
}
return Result.fromValue(batch.batchId());
String projectTitle = project.get().getProjectIntent().projectTitle().title();
return batchDomainService.register(label, isPilot, projectTitle, projectId);
}

public Result<BatchId, ResponseCode> addSampleToBatch(SampleId sampleId, BatchId batchId) {
Expand All @@ -48,11 +78,12 @@ public Result<BatchId, ResponseCode> addSampleToBatch(SampleId sampleId, BatchId
}
}


public enum ResponseCode {
BATCH_UPDATE_FAILED,
BATCH_NOT_FOUND,
BATCH_CREATION_FAILED

BATCH_CREATION_FAILED,
BATCH_REGISTRATION_FAILED
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package life.qbic.projectmanagement.application.policy;

import life.qbic.domain.concepts.DomainEventDispatcher;
import life.qbic.projectmanagement.application.policy.directive.InformUsersAboutBatchRegistration;

/**
* <b>Policy: Batch Registered</b>
* <p>
* A collection of all directives that need to be executed after a new batch of
* samples has been registered for measurement.
* <p>
* The policy subscribes to events of type
* {@link life.qbic.projectmanagement.domain.project.sample.event.BatchRegistered} and ensures the
* registration of all business required directives.
*
* @since 1.0.0
*/
public class BatchRegisteredPolicy {

private final InformUsersAboutBatchRegistration informUsers;

/**
* Creates an instance of a {@link BatchRegisteredPolicy} object.
* <p>
* All directives will be created and subscribed upon instantiation.
*
* @param informUsers directive to inform users of a project about the new samples of a batch
* {@link life.qbic.projectmanagement.domain.project.sample.Batch}
* @since 1.0.0
*/
public BatchRegisteredPolicy(InformUsersAboutBatchRegistration informUsers) {
this.informUsers = informUsers;
DomainEventDispatcher.instance().subscribe(this.informUsers);
}
}
Loading

0 comments on commit e744a26

Please sign in to comment.