Skip to content

Commit

Permalink
Merge pull request #1 from samply/feature/email
Browse files Browse the repository at this point in the history
Feature/email
  • Loading branch information
djuarezgf authored Dec 29, 2023
2 parents 03dafa9 + 6ec2eed commit e34713d
Show file tree
Hide file tree
Showing 43 changed files with 771 additions and 28 deletions.
28 changes: 28 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
### Java template
# Compiled class file
*.class

# Log file
*.log

# BlueJ files
*.ctxt

# Mobile Tools for Java (J2ME)
.mtj.tmp/

# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar

# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
replay_pid*

documents/
templates/
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,4 @@ mvnw
mvnw.cmd

documents/
templates/
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Bridgehead Operation
- Create query and draft project (Lens)
- humanReadable in Bridgehead Config
- Email Service
- Email Templates
- Email Sender Aspect
- Thymeleaf Template Engine Configuration
11 changes: 10 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<version>3.2.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>de.samply</groupId>
Expand All @@ -20,6 +20,7 @@
<lombok.version>1.18.30</lombok.version>
<maven-model.version>3.9.6</maven-model.version>
<spring-aop.version>6.1.1</spring-aop.version>
<byte-buddy.version>1.14.11</byte-buddy.version>
</properties>
<dependencies>
<dependency>
Expand All @@ -42,6 +43,14 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/de/samply/annotations/Email.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package de.samply.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Email {
}
19 changes: 19 additions & 0 deletions src/main/java/de/samply/annotations/EmailSender.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package de.samply.annotations;

import de.samply.notification.smtp.EmailRecipientType;
import de.samply.notification.smtp.EmailTemplateType;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface EmailSender {

// Recipients of the email
EmailRecipientType[] recipients() default {};

EmailTemplateType templateType();
}
233 changes: 233 additions & 0 deletions src/main/java/de/samply/aop/EmailSenderAspect.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
package de.samply.aop;

import de.samply.annotations.EmailSender;
import de.samply.db.model.Project;
import de.samply.db.model.ProjectBridgehead;
import de.samply.db.model.ProjectBridgeheadUser;
import de.samply.db.repository.*;
import de.samply.notification.smtp.EmailRecipient;
import de.samply.notification.smtp.EmailService;
import de.samply.notification.smtp.EmailServiceException;
import de.samply.security.SessionUser;
import de.samply.user.roles.OrganisationRoleToProjectRoleMapper;
import de.samply.user.roles.ProjectRole;
import de.samply.user.roles.UserProjectRoles;
import de.samply.utils.AspectUtils;
import de.samply.utils.ProjectRolesUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;

import java.util.*;

@Component
@Aspect
public class EmailSenderAspect {

private final EmailService emailService;
private final SessionUser sessionUser;
private final BridgeheadAdminUserRepository bridgeheadAdminUserRepository;
private final ProjectBridgeheadUserRepository projectBridgeheadUserRepository;
private final ProjectManagerAdminUserRepository projectManagerAdminUserRepository;
private final OrganisationRoleToProjectRoleMapper organisationRoleToProjectRoleMapper;
private final ProjectBridgeheadRepository projectBridgeheadRepository;
private final ProjectRepository projectRepository;

public EmailSenderAspect(EmailService emailService,
SessionUser sessionUser,
BridgeheadAdminUserRepository bridgeheadAdminUserRepository,
ProjectBridgeheadUserRepository projectBridgeheadUserRepository,
ProjectManagerAdminUserRepository projectManagerAdminUserRepository,
OrganisationRoleToProjectRoleMapper organisationRoleToProjectRoleMapper,
ProjectBridgeheadRepository projectBridgeheadRepository,
ProjectRepository projectRepository) {
this.emailService = emailService;
this.sessionUser = sessionUser;
this.bridgeheadAdminUserRepository = bridgeheadAdminUserRepository;
this.projectBridgeheadUserRepository = projectBridgeheadUserRepository;
this.projectManagerAdminUserRepository = projectManagerAdminUserRepository;
this.organisationRoleToProjectRoleMapper = organisationRoleToProjectRoleMapper;
this.projectBridgeheadRepository = projectBridgeheadRepository;
this.projectRepository = projectRepository;
}

@Pointcut("@annotation(de.samply.annotations.EmailSender)")
public void emailSenderPointcut() {
}

@Around("emailSenderPointcut()")
public Object aroundEmailSender(ProceedingJoinPoint joinPoint) throws Throwable {
try {
ResponseEntity responseEntity = (ResponseEntity) joinPoint.proceed();
if (responseEntity.getStatusCode().is2xxSuccessful()) {
sendEmail(joinPoint);
}
return responseEntity;
} catch (Exception e) {
throw new RuntimeException(e);
}
}

private void sendEmail(ProceedingJoinPoint joinPoint) {
fetchEmailSender(joinPoint).ifPresent(emailSender ->
fetchEmailRecipients(emailSender, joinPoint).forEach(emailRecipient -> sendEmail(emailRecipient, emailSender)));
}

private void sendEmail(EmailRecipient emailRecipient, EmailSender emailSender) {
try {
emailService.sendEmail(emailRecipient.email(), emailRecipient.bridgehead(), emailRecipient.role(), emailSender.templateType());
} catch (EmailServiceException e) {
throw new RuntimeException(e);
}
}

private Optional<EmailSender> fetchEmailSender(JoinPoint joinPoint) {
return AspectUtils.fetchT(joinPoint, EmailSender.class);
}

private Set<EmailRecipient> fetchEmailRecipients(EmailSender emailSender, ProceedingJoinPoint joinPoint) {
Set<EmailRecipient> result = new HashSet<>();
Optional<String> projectCode = AspectUtils.fetchProjectCode(joinPoint);
Optional<String> bridgehead = AspectUtils.fetchBridghead(joinPoint);
Optional<String> email = AspectUtils.fetchEmail(joinPoint);
Arrays.stream(emailSender.recipients()).forEach(emailRecipientType ->
result.addAll(switch (emailRecipientType) {
case SESSION_USER -> fetchEmailRecipientsForSessionUser(projectCode, bridgehead);
case EMAIL_ANNOTATION -> fetchEmailRecipientsForEmailAnnotation(projectCode, bridgehead, email);
case BRIDGEHEAD_ADMIN -> fetchEmailRecipientsForBridgeheadAdmin(projectCode, bridgehead);
case PROJECT_MANAGER_ADMIN -> fetchEmailRecipientsForProjectManagerAdmin();
case PROJECT_ALL -> fetchEmailRecipientsForAllProjectUsers(projectCode, bridgehead);
}));
return result;
}

private Set<EmailRecipient> fetchEmailRecipientsForSessionUser(Optional<String> projectCode, Optional<String> bridgehead) {
Set<EmailRecipient> result = new HashSet<>();
Optional<UserProjectRoles> userProjectRolesOptional = fetchSessionUserProjectRoles(projectCode);
userProjectRolesOptional.ifPresent(userProjectRoles -> {
ProjectRole projectRole = null;
if (bridgehead.isPresent()) {
List<ProjectRole> bridgeheadRolesOrderedInTimeDescendent = userProjectRoles.getBridgeheadRolesOrderedInDescendentTime(bridgehead.get());
if (!bridgeheadRolesOrderedInTimeDescendent.isEmpty()) {
projectRole = bridgeheadRolesOrderedInTimeDescendent.get(0);
}
}
if (projectRole == null) {
Set<ProjectRole> rolesNotDependentOnBridgeheads = userProjectRoles.getRolesNotDependentOnBridgeheads();
if (!rolesNotDependentOnBridgeheads.isEmpty()) {
projectRole = rolesNotDependentOnBridgeheads.stream().toList().get(0);
}
}
if (projectRole == null) {
projectRole = ProjectRole.DEFAULT;
}
result.add(new EmailRecipient(sessionUser.getEmail(), bridgehead, projectRole));
});
return result;
}

private Optional<UserProjectRoles> fetchSessionUserProjectRoles(Optional<String> projectCode) {
return (projectCode.isPresent()) ? organisationRoleToProjectRoleMapper.map(projectCode.get()) : Optional.empty();
}

private Set<EmailRecipient> fetchEmailRecipientsForEmailAnnotation(Optional<String> projectCode, Optional<String> bridgehead, Optional<String> email) {
Set<EmailRecipient> result = new HashSet<>();
if (email.isPresent()) {
ProjectRole projectRole = ProjectRole.DEFAULT;
if (projectCode.isPresent() && bridgehead.isPresent()) {
Optional<ProjectBridgeheadUser> projectBridgeheadUser = fetchProjectBridgeheadUser(projectCode.get(), bridgehead.get(), email.get());
if (projectBridgeheadUser.isPresent()) {
projectRole = projectBridgeheadUser.get().getProjectRole();
}
}
result.add(new EmailRecipient(email.get(), bridgehead, projectRole));
}
return result;
}

private Optional<ProjectBridgeheadUser> fetchProjectBridgeheadUser(String projectCode, String bridgehead, String email) {
Optional<Project> project = projectRepository.findByCode(projectCode);
if (project.isPresent()) {
Optional<ProjectBridgehead> projectBridgehead = projectBridgeheadRepository.findFirstByBridgeheadAndProject(bridgehead, project.get());
if (projectBridgehead.isPresent()) {
List<ProjectBridgeheadUser> projectBridgeheadUserList =
ProjectRolesUtils.orderCollectionInDescendentTime(
projectBridgeheadUserRepository.getByEmailAndProjectBridgehead(email, projectBridgehead.get()),
projectBridgeheadUser -> projectBridgeheadUser.getProjectRole());
if (!projectBridgeheadUserList.isEmpty()) {
return Optional.of(projectBridgeheadUserList.get(0));
}
}
}
return Optional.empty();
}

private Set<EmailRecipient> fetchEmailRecipientsForBridgeheadAdmin(Optional<String> projectCode, Optional<String> bridgehead) {
Set<EmailRecipient> result = new HashSet<>();
fetchProjectBridgeheads(projectCode, bridgehead).forEach(projectBridgehead ->
bridgeheadAdminUserRepository.findByBridgehead(projectBridgehead.getBridgehead()).forEach(bridgeheadAdminUser ->
result.add(new EmailRecipient(bridgeheadAdminUser.getEmail(), Optional.of(projectBridgehead.getBridgehead()), ProjectRole.BRIDGEHEAD_ADMIN))));
return result;
}

private Set<ProjectBridgehead> fetchProjectBridgeheads(Optional<String> projectCode, Optional<String> bridgehead) {
if (projectCode.isPresent()) {
Optional<Project> project = projectRepository.findByCode(projectCode.get());
if (project.isPresent()) {
if (bridgehead.isPresent()) {
Optional<ProjectBridgehead> projectBridgehead = projectBridgeheadRepository.findFirstByBridgeheadAndProject(bridgehead.get(), project.get());
if (projectBridgehead.isPresent()) {
return Set.of(projectBridgehead.get());
}
} else {
return projectBridgeheadRepository.findByProject(project.get());
}
}
}
return new HashSet<>();
}

private Set<EmailRecipient> fetchEmailRecipientsForProjectManagerAdmin() {
Set<EmailRecipient> result = new HashSet<>();
projectManagerAdminUserRepository.findAll().forEach(projectManagerAdminUser -> {
result.add(new EmailRecipient(projectManagerAdminUser.getEmail(), Optional.empty(), ProjectRole.PROJECT_MANAGER_ADMIN));
});
return result;
}

private Set<EmailRecipient> fetchEmailRecipientsForAllProjectUsers(Optional<String> projectCode, Optional<String> bridgehead) {
Map<String, EmailRecipient> userEmailRecipientMap = new HashMap<>();
fetchProjectBridgeheads(projectCode, bridgehead).forEach(projectBridgehead -> {
projectBridgeheadUserRepository.getByProjectBridgehead(projectBridgehead).forEach(projectBridgeheadUser -> {
boolean addUser;
EmailRecipient emailRecipient = userEmailRecipientMap.get(projectBridgeheadUser.getEmail());
if (emailRecipient != null) {
addUser = ProjectRolesUtils.compare(emailRecipient.role(), projectBridgeheadUser.getProjectRole()) > 0;
} else {
addUser = true;
}
if (addUser) {
userEmailRecipientMap.put(projectBridgeheadUser.getEmail(),
new EmailRecipient(projectBridgeheadUser.getEmail(), Optional.of(projectBridgehead.getBridgehead()), projectBridgeheadUser.getProjectRole()));
}
});
bridgeheadAdminUserRepository.findByBridgehead(projectBridgehead.getBridgehead()).forEach(bridgeheadAdminUser -> {
if (!userEmailRecipientMap.containsKey(bridgeheadAdminUser.getEmail())) {
userEmailRecipientMap.put(bridgeheadAdminUser.getEmail(), new EmailRecipient(bridgeheadAdminUser.getEmail(), Optional.of(bridgeheadAdminUser.getBridgehead()), ProjectRole.BRIDGEHEAD_ADMIN));
}
});
});
projectManagerAdminUserRepository.findAll().forEach(projectManagerAdminUser -> {
if (!userEmailRecipientMap.containsKey(projectManagerAdminUser.getEmail())) {
userEmailRecipientMap.put(projectManagerAdminUser.getEmail(), new EmailRecipient(projectManagerAdminUser.getEmail(), Optional.empty(), ProjectRole.PROJECT_MANAGER_ADMIN));
}
});
return new HashSet<>(userEmailRecipientMap.values());
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public Object aroundRoleConstraints(ProceedingJoinPoint joinPoint) throws Throwa
}

private Optional<RoleConstraints> fetchRoleConstrains(JoinPoint joinPoint) {
return Optional.of(AspectUtils.fetchMethod(joinPoint).getAnnotation(RoleConstraints.class));
return AspectUtils.fetchT(joinPoint, RoleConstraints.class);
}


Expand Down
2 changes: 1 addition & 1 deletion src/main/java/de/samply/aop/StateConstraintsAspect.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public Object aroundStateConstraints(ProceedingJoinPoint joinPoint) throws Throw
}

private Optional<StateConstraints> fetchStateConstrains(JoinPoint joinPoint) {
return Optional.of(AspectUtils.fetchMethod(joinPoint).getAnnotation(StateConstraints.class));
return AspectUtils.fetchT(joinPoint, StateConstraints.class);
}


Expand Down
Loading

0 comments on commit e34713d

Please sign in to comment.