diff --git a/dmp-backend/data/src/main/java/eu/eudat/data/dao/criteria/NotificationCriteria.java b/dmp-backend/data/src/main/java/eu/eudat/data/dao/criteria/NotificationCriteria.java new file mode 100644 index 000000000..1cd361014 --- /dev/null +++ b/dmp-backend/data/src/main/java/eu/eudat/data/dao/criteria/NotificationCriteria.java @@ -0,0 +1,26 @@ +package eu.eudat.data.dao.criteria; + +import eu.eudat.data.enumeration.notification.ActiveStatus; +import eu.eudat.data.enumeration.notification.NotifyState; + +public class NotificationCriteria { + + private ActiveStatus isActive; + private NotifyState notifyState; + + public ActiveStatus getIsActive() { + return isActive; + } + + public void setIsActive(ActiveStatus isActive) { + this.isActive = isActive; + } + + public NotifyState getNotifyState() { + return notifyState; + } + + public void setNotifyState(NotifyState notifyState) { + this.notifyState = notifyState; + } +} diff --git a/dmp-backend/data/src/main/java/eu/eudat/data/dao/entities/NotificationDao.java b/dmp-backend/data/src/main/java/eu/eudat/data/dao/entities/NotificationDao.java new file mode 100644 index 000000000..508e03834 --- /dev/null +++ b/dmp-backend/data/src/main/java/eu/eudat/data/dao/entities/NotificationDao.java @@ -0,0 +1,13 @@ +package eu.eudat.data.dao.entities; + +import eu.eudat.data.dao.DatabaseAccessLayer; +import eu.eudat.data.dao.criteria.NotificationCriteria; +import eu.eudat.data.entities.Notification; +import eu.eudat.queryable.QueryableList; + +import java.util.UUID; + +public interface NotificationDao extends DatabaseAccessLayer { + + QueryableList getWithCriteria(NotificationCriteria criteria); +} diff --git a/dmp-backend/data/src/main/java/eu/eudat/data/dao/entities/NotificationDaoImpl.java b/dmp-backend/data/src/main/java/eu/eudat/data/dao/entities/NotificationDaoImpl.java new file mode 100644 index 000000000..17fa9dd18 --- /dev/null +++ b/dmp-backend/data/src/main/java/eu/eudat/data/dao/entities/NotificationDaoImpl.java @@ -0,0 +1,61 @@ +package eu.eudat.data.dao.entities; + +import eu.eudat.data.dao.DatabaseAccess; +import eu.eudat.data.dao.criteria.NotificationCriteria; +import eu.eudat.data.dao.databaselayer.service.DatabaseService; +import eu.eudat.data.entities.Lock; +import eu.eudat.data.entities.Notification; +import eu.eudat.queryable.QueryableList; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +@Service("NotificationDao") +public class NotificationDaoImpl extends DatabaseAccess implements NotificationDao { + @Autowired + public NotificationDaoImpl(DatabaseService databaseService) { + super(databaseService); + } + + @Override + public QueryableList getWithCriteria(NotificationCriteria criteria) { + QueryableList query = this.getDatabaseService().getQueryable(Notification.class); + if (criteria.getIsActive() != null) + query.where((builder, root) -> builder.equal(root.get("isActive"), criteria.getIsActive())); + if (criteria.getNotifyState() != null) + query.where(((builder, root) -> builder.equal(root.get("notifyState"), criteria.getNotifyState()))); + return query; + } + + @Override + public Notification createOrUpdate(Notification item) { + return this.getDatabaseService().createOrUpdate(item, Notification.class); + } + + @Override + public CompletableFuture createOrUpdateAsync(Notification item) { + return CompletableFuture.supplyAsync(() -> this.getDatabaseService().createOrUpdate(item, Notification.class)); + } + + @Override + public Notification find(UUID id) { + return this.getDatabaseService().getQueryable(Notification.class).where(((builder, root) -> builder.equal(root.get("id"), id))).getSingle(); + } + + @Override + public Notification find(UUID id, String hint) { + throw new UnsupportedOperationException(); + } + + @Override + public void delete(Notification item) { + this.getDatabaseService().delete(item); + } + + @Override + public QueryableList asQueryable() { + return this.getDatabaseService().getQueryable(Notification.class); + } +} diff --git a/dmp-backend/data/src/main/java/eu/eudat/data/entities/Notification.java b/dmp-backend/data/src/main/java/eu/eudat/data/entities/Notification.java new file mode 100644 index 000000000..39eb094a8 --- /dev/null +++ b/dmp-backend/data/src/main/java/eu/eudat/data/entities/Notification.java @@ -0,0 +1,175 @@ +package eu.eudat.data.entities; + +import eu.eudat.data.enumeration.notification.ActiveStatus; +import eu.eudat.data.enumeration.notification.ContactType; +import eu.eudat.data.enumeration.notification.NotificationType; +import eu.eudat.data.enumeration.notification.NotifyState; +import eu.eudat.queryable.queryableentity.DataEntity; +import org.hibernate.annotations.GenericGenerator; + +import javax.persistence.*; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +@Entity +@Table(name = "\"Notification\"") +public class Notification implements DataEntity { + + @Id + @GeneratedValue + @GenericGenerator(name = "uuid2", strategy = "uuid2") + @Column(name = "id", updatable = false, nullable = false, columnDefinition = "BINARY(16)") + private UUID id; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "\"UserId\"") + private UserInfo userId; + + @Enumerated + @Column(name = "\"IsActive\"", nullable = false) + private ActiveStatus isActive; + + @Enumerated + @Column(name = "\"Type\"", nullable = false) + private NotificationType type; + + @Enumerated + @Column(name = "\"ContactTypeHint\"") + private ContactType contactTypeHint; + + @Column(name = "\"ContactHint\"") + private String contactHint; + + @Column(name = "\"Data\"") + private String data; + + @Enumerated + @Column(name = "\"NotifyState\"") + private NotifyState notifyState; + + @Column(name = "\"NotifiedAt\"") + private Date notifiedAt; + + @Column(name = "\"RetryCount\"") + private Integer retryCount; + + @Column(name = "\"CreatedAt\"") + private Date createdAt; + + @Column(name = "\"UpdatedAt\"") + private Date updatedAt; + + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public UserInfo getUserId() { + return userId; + } + + public void setUserId(UserInfo userId) { + this.userId = userId; + } + + public ActiveStatus getIsActive() { + return isActive; + } + + public void setIsActive(ActiveStatus isActive) { + this.isActive = isActive; + } + + public NotificationType getType() { + return type; + } + + public void setType(NotificationType type) { + this.type = type; + } + + public ContactType getContactTypeHint() { + return contactTypeHint; + } + + public void setContactTypeHint(ContactType contactTypeHint) { + this.contactTypeHint = contactTypeHint; + } + + public String getContactHint() { + return contactHint; + } + + public void setContactHint(String contactHint) { + this.contactHint = contactHint; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + + public NotifyState getNotifyState() { + return notifyState; + } + + public void setNotifyState(NotifyState notifyState) { + this.notifyState = notifyState; + } + + public Date getNotifiedAt() { + return notifiedAt; + } + + public void setNotifiedAt(Date notifiedAt) { + this.notifiedAt = notifiedAt; + } + + public Integer getRetryCount() { + return retryCount; + } + + public void setRetryCount(Integer retryCount) { + this.retryCount = retryCount; + } + + public Date getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Date createdAt) { + this.createdAt = createdAt; + } + + public Date getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Date updatedAt) { + this.updatedAt = updatedAt; + } + + @Override + public void update(Notification entity) { + } + + @Override + public UUID getKeys() { + return null; + } + + @Override + public Notification buildFromTuple(List tuple, List fields, String base) { + return null; + } + + +} diff --git a/dmp-backend/data/src/main/java/eu/eudat/data/entities/UserInfo.java b/dmp-backend/data/src/main/java/eu/eudat/data/entities/UserInfo.java index 533791303..ecd4263a5 100644 --- a/dmp-backend/data/src/main/java/eu/eudat/data/entities/UserInfo.java +++ b/dmp-backend/data/src/main/java/eu/eudat/data/entities/UserInfo.java @@ -72,6 +72,9 @@ public class UserInfo implements DataEntity { @OneToMany(mappedBy = "lockedBy", fetch = FetchType.LAZY) private Set locks = new HashSet<>(); + @OneToMany(mappedBy = "userId", fetch = FetchType.LAZY) + private Set notifications = new HashSet<>(); + public Set getDmps() { return dmps; } @@ -176,6 +179,14 @@ public class UserInfo implements DataEntity { this.locks = locks; } + public Set getNotifications() { + return notifications; + } + + public void setNotifications(Set notifications) { + this.notifications = notifications; + } + @Override public void update(UserInfo entity) { this.name = entity.getName(); diff --git a/dmp-backend/data/src/main/java/eu/eudat/data/enumeration/notification/ActiveStatus.java b/dmp-backend/data/src/main/java/eu/eudat/data/enumeration/notification/ActiveStatus.java new file mode 100644 index 000000000..1fa16c2d8 --- /dev/null +++ b/dmp-backend/data/src/main/java/eu/eudat/data/enumeration/notification/ActiveStatus.java @@ -0,0 +1,27 @@ +package eu.eudat.data.enumeration.notification; + +public enum ActiveStatus { + ACTIVE(0), + INACTIVE(1); + + private int status; + + ActiveStatus(int status) { + this.status = status; + } + + public int getStatus() { + return status; + } + + public ActiveStatus fromInteger(int status) { + switch (status) { + case 0: + return ACTIVE; + case 1: + return INACTIVE; + default: + throw new RuntimeException("Unsupported Active Status"); + } + } +} diff --git a/dmp-backend/data/src/main/java/eu/eudat/data/enumeration/notification/ContactType.java b/dmp-backend/data/src/main/java/eu/eudat/data/enumeration/notification/ContactType.java new file mode 100644 index 000000000..3bbbceec9 --- /dev/null +++ b/dmp-backend/data/src/main/java/eu/eudat/data/enumeration/notification/ContactType.java @@ -0,0 +1,24 @@ +package eu.eudat.data.enumeration.notification; + +public enum ContactType { + EMAIL(0); + + private int type; + + ContactType(int type) { + this.type = type; + } + + public int getType() { + return type; + } + + public ContactType fromInteger(int type) { + switch (type) { + case 0: + return EMAIL; + default: + throw new RuntimeException("Unsupported Contact Type"); + } + } +} diff --git a/dmp-backend/data/src/main/java/eu/eudat/data/enumeration/notification/NotificationType.java b/dmp-backend/data/src/main/java/eu/eudat/data/enumeration/notification/NotificationType.java new file mode 100644 index 000000000..392fe3bd5 --- /dev/null +++ b/dmp-backend/data/src/main/java/eu/eudat/data/enumeration/notification/NotificationType.java @@ -0,0 +1,27 @@ +package eu.eudat.data.enumeration.notification; + +public enum NotificationType { + DMP_MODIFIED(0), + DATASET_MODIFIED(1); + + private int type; + + NotificationType(int type) { + this.type = type; + } + + public int getType() { + return type; + } + + public NotificationType fromInteger(int type) { + switch (type) { + case 0: + return DMP_MODIFIED; + case 1: + return DATASET_MODIFIED; + default: + throw new RuntimeException("Unsupported Notification Type"); + } + } +} diff --git a/dmp-backend/data/src/main/java/eu/eudat/data/enumeration/notification/NotifyState.java b/dmp-backend/data/src/main/java/eu/eudat/data/enumeration/notification/NotifyState.java new file mode 100644 index 000000000..b7e75317f --- /dev/null +++ b/dmp-backend/data/src/main/java/eu/eudat/data/enumeration/notification/NotifyState.java @@ -0,0 +1,36 @@ +package eu.eudat.data.enumeration.notification; + +public enum NotifyState { + PENDING(0), + PROCESSING(1), + SENDING(2), + SUCCEEDED(3), + ERROR(4); + + private int state; + + NotifyState(int state) { + this.state = state; + } + + public int getState() { + return state; + } + + public NotifyState fromInteger(int state) { + switch (state) { + case 0: + return PENDING; + case 1: + return PROCESSING; + case 2: + return SENDING; + case 3: + return SUCCEEDED; + case 4: + return ERROR; + default: + throw new RuntimeException("Unsupported Notify State"); + } + } +} diff --git a/dmp-backend/web/src/main/java/eu/eudat/configurations/WebMVCConfiguration.java b/dmp-backend/web/src/main/java/eu/eudat/configurations/WebMVCConfiguration.java index 89a330cd2..36d9c502c 100644 --- a/dmp-backend/web/src/main/java/eu/eudat/configurations/WebMVCConfiguration.java +++ b/dmp-backend/web/src/main/java/eu/eudat/configurations/WebMVCConfiguration.java @@ -7,6 +7,7 @@ import eu.eudat.logic.services.operations.authentication.AuthenticationService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; @@ -15,6 +16,7 @@ import java.util.List; @EnableAsync @Configuration +@EnableScheduling public class WebMVCConfiguration extends WebMvcConfigurerAdapter { private ApiContext apiContext; diff --git a/dmp-backend/web/src/main/java/eu/eudat/logic/managers/DataManagementPlanManager.java b/dmp-backend/web/src/main/java/eu/eudat/logic/managers/DataManagementPlanManager.java index f4ac98b30..3da078abd 100644 --- a/dmp-backend/web/src/main/java/eu/eudat/logic/managers/DataManagementPlanManager.java +++ b/dmp-backend/web/src/main/java/eu/eudat/logic/managers/DataManagementPlanManager.java @@ -9,6 +9,10 @@ import eu.eudat.data.dao.entities.*; import eu.eudat.data.entities.Organisation; import eu.eudat.data.entities.Researcher; import eu.eudat.data.entities.*; +import eu.eudat.data.enumeration.notification.ActiveStatus; +import eu.eudat.data.enumeration.notification.ContactType; +import eu.eudat.data.enumeration.notification.NotificationType; +import eu.eudat.data.enumeration.notification.NotifyState; import eu.eudat.data.query.items.item.dmp.DataManagementPlanCriteriaRequest; import eu.eudat.data.query.items.table.datasetprofile.DatasetProfileTableRequestItem; import eu.eudat.data.query.items.table.dmp.DataManagementPlanTableRequest; @@ -474,7 +478,7 @@ public class DataManagementPlanManager { } public DMP createOrUpdate(ApiContext apiContext, DataManagementPlanEditorModel dataManagementPlan, Principal principal) throws Exception { - + boolean setNotification = false; if (dataManagementPlan.getId() != null) { DMP dmp1 = apiContext.getOperationsContext().getDatabaseRepository().getDmpDao().find(dataManagementPlan.getId()); @@ -491,6 +495,8 @@ public class DataManagementPlanManager { } if (dataManagementPlan.getStatus() == (int) DMP.DMPStatus.FINALISED.getValue() && dmp1.getStatus().equals(DMP.DMPStatus.FINALISED.getValue())) throw new Exception("DMP is finalized, therefore cannot be edited."); + + setNotification = true; } DMP newDmp = dataManagementPlan.toDataModel(); @@ -568,9 +574,38 @@ public class DataManagementPlanManager { if (dataManagementPlan.getAssociatedUsers().size() == 0) assignUser(newDmp, user); + if (setNotification) { + this.sendNotification(newDmp, user); + } + return newDmp; } + private void sendNotification(DMP dmp, UserInfo user) { + List userDMPS = databaseRepository.getUserDmpDao().asQueryable().where(((builder, root) -> builder.equal(root.get("dmp").get("id"), dmp.getId()))).toList(); + for (UserDMP userDMP : userDMPS) { + if (!userDMP.getUser().getId().equals(user.getId())) { + Notification notification = new Notification(); + notification.setUserId(user); + notification.setType(NotificationType.DMP_MODIFIED); + notification.setNotifyState(NotifyState.PENDING); + notification.setIsActive(ActiveStatus.ACTIVE); + notification.setData("{" + + "\"userId\": \"" + userDMP.getUser().getId() + "\"" + + ", \"id\": \"" + userDMP.getDmp().getId() + "\"" + + ", \"name\": \"" + userDMP.getDmp().getLabel() + "\"" + + ", \"path\": \"/plans/edit\"" + + "}"); + notification.setCreatedAt(new Date()); + notification.setUpdatedAt(notification.getCreatedAt()); + notification.setContactTypeHint(ContactType.EMAIL); + notification.setContactHint(userDMP.getUser().getEmail()); + databaseRepository.getNotificationDao().createOrUpdate(notification); + } + } + + } + private void assignUser(DMP dmp, UserInfo userInfo) { UserDMP userDMP = new UserDMP(); diff --git a/dmp-backend/web/src/main/java/eu/eudat/logic/managers/DatasetManager.java b/dmp-backend/web/src/main/java/eu/eudat/logic/managers/DatasetManager.java index 8949d2c74..ab1b1d862 100644 --- a/dmp-backend/web/src/main/java/eu/eudat/logic/managers/DatasetManager.java +++ b/dmp-backend/web/src/main/java/eu/eudat/logic/managers/DatasetManager.java @@ -5,6 +5,10 @@ import eu.eudat.data.dao.entities.DataRepositoryDao; import eu.eudat.data.dao.entities.DatasetDao; import eu.eudat.data.dao.entities.RegistryDao; import eu.eudat.data.entities.*; +import eu.eudat.data.enumeration.notification.ActiveStatus; +import eu.eudat.data.enumeration.notification.ContactType; +import eu.eudat.data.enumeration.notification.NotificationType; +import eu.eudat.data.enumeration.notification.NotifyState; import eu.eudat.data.query.items.table.dataset.DatasetPublicTableRequest; import eu.eudat.data.query.items.table.dataset.DatasetTableRequest; import eu.eudat.data.query.items.table.datasetprofile.DatasetProfileTableRequestItem; @@ -400,6 +404,7 @@ public class DatasetManager { } public eu.eudat.data.entities.Dataset createOrUpdate(DatasetWizardModel datasetWizardModel, Principal principal) throws Exception { + Boolean sendNotification = false; DMP dmp = apiContext.getOperationsContext().getDatabaseRepository().getDmpDao().find(datasetWizardModel.getDmp().getId()); if (datasetWizardModel.getId() != null) { Dataset tempDataset = apiContext.getOperationsContext().getDatabaseRepository().getDatasetDao().find(datasetWizardModel.getId()); @@ -407,6 +412,7 @@ public class DatasetManager { if (datasetWizardModel.getModified().getTime() != tempDataset.getModified().getTime()) { throw new Exception("Dataset has been modified already by another user."); } + sendNotification = true; } } if (dmp.getStatus().equals(DMP.DMPStatus.FINALISED.getValue()) && datasetWizardModel.getId() != null) @@ -427,9 +433,37 @@ public class DatasetManager { Dataset dataset1 = apiContext.getOperationsContext().getDatabaseRepository().getDatasetDao().createOrUpdate(dataset); datasetWizardModel.setId(dataset1.getId()); updateTags(apiContext.getOperationsContext().getDatasetRepository(), datasetWizardModel); + if (sendNotification) { + this.sendNotification(dataset1, dataset1.getDmp(), userInfo); + } return dataset1; } + private void sendNotification(Dataset dataset, DMP dmp, UserInfo user) { + List userDMPS = databaseRepository.getUserDmpDao().asQueryable().where(((builder, root) -> builder.equal(root.get("dmp").get("id"), dmp.getId()))).toList(); + for (UserDMP userDMP : userDMPS) { + if (!userDMP.getUser().getId().equals(user.getId())) { + Notification notification = new Notification(); + notification.setUserId(user); + notification.setType(NotificationType.DATASET_MODIFIED); + notification.setNotifyState(NotifyState.PENDING); + notification.setIsActive(ActiveStatus.ACTIVE); + notification.setData("{" + + "\"userId\": \"" + userDMP.getUser().getId() + "\"" + + ", \"id\": \"" + dataset.getId() + "\"" + + ", \"name\": \"" + dataset.getLabel() + "\"" + + ", \"path\": \"/datasets/edit\"" + + "}"); + notification.setCreatedAt(new Date()); + notification.setUpdatedAt(notification.getCreatedAt()); + notification.setContactTypeHint(ContactType.EMAIL); + notification.setContactHint(userDMP.getUser().getEmail()); + databaseRepository.getNotificationDao().createOrUpdate(notification); + } + } + + } + private void checkDatasetValidation(Dataset dataset) throws Exception { List datasetProfileValidators = new LinkedList<>(); DatasetProfile profile = apiContext.getOperationsContext().getDatabaseRepository().getDatasetProfileDao().find(dataset.getProfile().getId()); diff --git a/dmp-backend/web/src/main/java/eu/eudat/logic/managers/NotificationManager.java b/dmp-backend/web/src/main/java/eu/eudat/logic/managers/NotificationManager.java new file mode 100644 index 000000000..a4c8c2e11 --- /dev/null +++ b/dmp-backend/web/src/main/java/eu/eudat/logic/managers/NotificationManager.java @@ -0,0 +1,118 @@ +package eu.eudat.logic.managers; + +import com.fasterxml.jackson.databind.ObjectMapper; +import eu.eudat.data.entities.Notification; +import eu.eudat.data.entities.UserInfo; +import eu.eudat.data.enumeration.notification.ActiveStatus; +import eu.eudat.data.enumeration.notification.NotificationType; +import eu.eudat.data.enumeration.notification.NotifyState; +import eu.eudat.logic.services.ApiContext; +import eu.eudat.logic.services.utilities.MailService; +import eu.eudat.models.data.mail.SimpleMail; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +import javax.mail.MessagingException; +import javax.transaction.Transactional; +import java.io.IOException; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +@Component +public class NotificationManager { + private static final Logger logger = LoggerFactory.getLogger(NotificationManager.class); + + private ApiContext apiContext; + private Environment environment; + private MailService mailService; + + @Autowired + public NotificationManager(ApiContext apiContext, Environment environment, MailService mailService) { + this.apiContext = apiContext; + this.environment = environment; + this.mailService = mailService; + } + + @Transactional + public void sendNotification(Notification notification) throws Exception { + if (notification.getNotifyState() == NotifyState.ERROR) { + if (notification.getRetryCount() == null) { + notification.setRetryCount(0); + } + notification.setRetryCount(notification.getRetryCount() + 1); + if (notification.getRetryCount() >= this.environment.getProperty("notification.maxRetries", Integer.class)) { + notification.setIsActive(ActiveStatus.INACTIVE); + notification.setUpdatedAt(new Date()); + return; + } + } + notification.setNotifyState(NotifyState.PROCESSING); + notification.setNotifiedAt(new Date()); + notification.setUpdatedAt(new Date()); + try { + Map data = new ObjectMapper().readValue(notification.getData(), HashMap.class); + UserInfo userInfo = this.apiContext.getOperationsContext().getDatabaseRepository().getUserInfoDao().find(UUID.fromString(data.get("userId"))); + String subjectTemplate = ""; + String contentTemplate = ""; + + switch (notification.getType()) { + case DMP_MODIFIED: + case DATASET_MODIFIED: + subjectTemplate = this.environment.getProperty("mail.modified.notification.subject"); + contentTemplate = mailService.getMailTemplateContent("classpath:modifiedNotification.html"); + break; + } + + + switch (notification.getContactTypeHint()) { + case EMAIL: + this.sendEmailNotification(notification, userInfo, data, subjectTemplate, contentTemplate); + notification.setNotifyState(NotifyState.SUCCEEDED); + notification.setUpdatedAt(new Date()); + break; + } + }catch (Exception e) { + notification.setNotifyState(NotifyState.ERROR); + notification.setUpdatedAt(new Date()); + logger.error(e.getMessage(), e); + } + } + + private void sendEmailNotification(Notification notification, UserInfo userInfo, Map data, String subjectTemplate, String contentTemplate) throws IOException { + CompletableFuture.runAsync(() -> { + SimpleMail simpleMail = new SimpleMail(); + simpleMail.setFrom(this.environment.getProperty("mail.from")); + simpleMail.setSubject(makeSubject(data, subjectTemplate)); + simpleMail.setTo(notification.getContactHint()); + simpleMail.setContent(makeContent(data, notification, userInfo, contentTemplate)); + try { + mailService.sendSimpleMail(simpleMail); + } catch (MessagingException e) { + notification.setNotifyState(NotifyState.ERROR); + notification.setUpdatedAt(new Date()); + logger.error(e.getMessage(), e); + } + }); + } + + private String makeSubject(Map data, String subjectTemplate) { + return subjectTemplate.replace("{name}", data.get("name")); + } + + private String makeContent(Map data, Notification notification, UserInfo userInfo, String template) { + String content = template; + content = content.replace("{recipient}", userInfo.getName()); + content = content.replace("{id}", data.get("id")); + content = content.replace("{name}", data.get("name")); + content = content.replace("{host}", this.environment.getProperty("dmp.domain")); + content = content.replace("{reasonName}", notification.getUserId().getName()); + content = content.replace("{path}", data.get("path")); + return content; + } +} diff --git a/dmp-backend/web/src/main/java/eu/eudat/logic/services/operations/DatabaseRepository.java b/dmp-backend/web/src/main/java/eu/eudat/logic/services/operations/DatabaseRepository.java index 406e12dce..539e417a6 100644 --- a/dmp-backend/web/src/main/java/eu/eudat/logic/services/operations/DatabaseRepository.java +++ b/dmp-backend/web/src/main/java/eu/eudat/logic/services/operations/DatabaseRepository.java @@ -54,5 +54,7 @@ public interface DatabaseRepository { LockDao getLockDao(); + NotificationDao getNotificationDao(); + void detachEntity(T entity); } diff --git a/dmp-backend/web/src/main/java/eu/eudat/logic/services/operations/DatabaseRepositoryImpl.java b/dmp-backend/web/src/main/java/eu/eudat/logic/services/operations/DatabaseRepositoryImpl.java index 730d5ccc2..a87ba7734 100644 --- a/dmp-backend/web/src/main/java/eu/eudat/logic/services/operations/DatabaseRepositoryImpl.java +++ b/dmp-backend/web/src/main/java/eu/eudat/logic/services/operations/DatabaseRepositoryImpl.java @@ -36,6 +36,7 @@ public class DatabaseRepositoryImpl implements DatabaseRepository { private ProjectDao projectDao; private FunderDao funderDao; private LockDao lockDao; + private NotificationDao notificationDao; private EntityManager entityManager; @@ -284,6 +285,16 @@ public class DatabaseRepositoryImpl implements DatabaseRepository { return lockDao; } + @Override + public NotificationDao getNotificationDao() { + return notificationDao; + } + + @Autowired + public void setNotificationDao(NotificationDao notificationDao) { + this.notificationDao = notificationDao; + } + public void detachEntity(T entity) { this.entityManager.detach(entity); } diff --git a/dmp-backend/web/src/main/java/eu/eudat/logic/utilities/schedule/notification/NotificationScheduleJob.java b/dmp-backend/web/src/main/java/eu/eudat/logic/utilities/schedule/notification/NotificationScheduleJob.java new file mode 100644 index 000000000..3d9f04cdd --- /dev/null +++ b/dmp-backend/web/src/main/java/eu/eudat/logic/utilities/schedule/notification/NotificationScheduleJob.java @@ -0,0 +1,48 @@ +package eu.eudat.logic.utilities.schedule.notification; + +import eu.eudat.data.entities.Notification; +import eu.eudat.data.enumeration.notification.ActiveStatus; +import eu.eudat.data.enumeration.notification.NotifyState; +import eu.eudat.logic.managers.NotificationManager; +import eu.eudat.logic.services.ApiContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import javax.transaction.Transactional; +import java.util.List; + +@Component +public class NotificationScheduleJob { + private static final Logger logger = LoggerFactory.getLogger(NotificationScheduleJob.class); + + private ApiContext apiContext; + private NotificationManager notificationManager; + + @Autowired + public NotificationScheduleJob(ApiContext apiContext, NotificationManager notificationManager) { + this.apiContext = apiContext; + this.notificationManager = notificationManager; + } + + @Transactional + @Scheduled(fixedRateString = "${notification.rateInterval}") + public void sendNotifications() { + List notifications = this.apiContext.getOperationsContext().getDatabaseRepository().getNotificationDao().asQueryable().where(((builder, root) -> + builder.and( + builder.or( + builder.equal(root.get("notifyState"), NotifyState.PENDING), builder.equal(root.get("notifyState"), NotifyState.ERROR)) + , builder.equal(root.get("isActive"), ActiveStatus.ACTIVE)))).toList(); + if (!notifications.isEmpty()) { + notifications.forEach(notification -> { + try { + this.notificationManager.sendNotification(notification); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } + }); + } + } +} diff --git a/dmp-backend/web/src/main/java/eu/eudat/models/data/notiication/Notification.java b/dmp-backend/web/src/main/java/eu/eudat/models/data/notiication/Notification.java new file mode 100644 index 000000000..4ee5d13f4 --- /dev/null +++ b/dmp-backend/web/src/main/java/eu/eudat/models/data/notiication/Notification.java @@ -0,0 +1,163 @@ +package eu.eudat.models.data.notiication; + +import eu.eudat.data.enumeration.notification.ActiveStatus; +import eu.eudat.data.enumeration.notification.ContactType; +import eu.eudat.data.enumeration.notification.NotificationType; +import eu.eudat.data.enumeration.notification.NotifyState; +import eu.eudat.models.DataModel; +import eu.eudat.models.data.userinfo.UserInfo; + +import java.util.Date; +import java.util.UUID; + +public class Notification implements DataModel { + + private UUID id; + private UserInfo userId; + private ActiveStatus isActive; + private NotificationType type; + private ContactType contactTypeHint; + private String contactHint; + private String data; + private NotifyState notifyState; + private Date notifiedAt; + private Integer retryCount; + private Date createdAt; + private Date updatedAt; + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public UserInfo getUserId() { + return userId; + } + + public void setUserId(UserInfo userId) { + this.userId = userId; + } + + public ActiveStatus getIsActive() { + return isActive; + } + + public void setIsActive(ActiveStatus isActive) { + this.isActive = isActive; + } + + public NotificationType getType() { + return type; + } + + public void setType(NotificationType type) { + this.type = type; + } + + public ContactType getContactTypeHint() { + return contactTypeHint; + } + + public void setContactTypeHint(ContactType contactTypeHint) { + this.contactTypeHint = contactTypeHint; + } + + public String getContactHint() { + return contactHint; + } + + public void setContactHint(String contactHint) { + this.contactHint = contactHint; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + + public NotifyState getNotifyState() { + return notifyState; + } + + public void setNotifyState(NotifyState notifyState) { + this.notifyState = notifyState; + } + + public Date getNotifiedAt() { + return notifiedAt; + } + + public void setNotifiedAt(Date notifiedAt) { + this.notifiedAt = notifiedAt; + } + + public Integer getRetryCount() { + return retryCount; + } + + public void setRetryCount(Integer retryCount) { + this.retryCount = retryCount; + } + + public Date getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Date createdAt) { + this.createdAt = createdAt; + } + + public Date getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Date updatedAt) { + this.updatedAt = updatedAt; + } + + @Override + public Notification fromDataModel(eu.eudat.data.entities.Notification entity) { + this.id = entity.getId(); + this.contactHint = entity.getContactHint(); + this.contactTypeHint = entity.getContactTypeHint(); + this.createdAt = entity.getCreatedAt(); + this.data = entity.getData(); + this.isActive = entity.getIsActive(); + this.notifiedAt = entity.getNotifiedAt(); + this.notifyState = entity.getNotifyState(); + this.retryCount = entity.getRetryCount(); + this.type = entity.getType(); + this.updatedAt = entity.getUpdatedAt(); + this.userId = new UserInfo().fromDataModel(entity.getUserId()); + return this; + } + + @Override + public eu.eudat.data.entities.Notification toDataModel() throws Exception { + eu.eudat.data.entities.Notification entity = new eu.eudat.data.entities.Notification(); + entity.setId(this.id); + entity.setContactHint(this.contactHint); + entity.setContactTypeHint(this.contactTypeHint); + entity.setCreatedAt(this.createdAt); + entity.setData(this.data); + entity.setIsActive(this.isActive); + entity.setNotifiedAt(this.notifiedAt); + entity.setNotifyState(this.notifyState); + entity.setRetryCount(this.retryCount); + entity.setType(this.type); + entity.setUpdatedAt(this.updatedAt); + entity.setUserId(this.userId.toDataModel()); + return entity; + } + + @Override + public String getHint() { + return null; + } +} diff --git a/dmp-backend/web/src/main/resources/application.properties b/dmp-backend/web/src/main/resources/application.properties index 93e9aca84..6f76429c6 100644 --- a/dmp-backend/web/src/main/resources/application.properties +++ b/dmp-backend/web/src/main/resources/application.properties @@ -9,6 +9,7 @@ eu.eudat.logic.proxy.allowed.host=https://eestore.paas2.uninett.no ####################GENERIC MAIL CONFIGURATIONS################# mail.subject=Invitation to DMP Plan {dmpname} mail.from=TheApp@dev.cite.gr +mail.modified.notification.subject=[OpenDMP] The {name} has been modified ####################SPRING MAIL CONFIGURATIONS################# spring.mail.default-encoding=UTF-8 @@ -76,3 +77,8 @@ http-logger.delay = 10 #############GENERIC DATASOURCE CONFIGURATIONS######### database.driver-class-name=org.postgresql.Driver database.lock-fail-interval=120000 + +userguide.path=guide/ + +notification.rateInterval=30000 +notification.maxRetries=10 diff --git a/dmp-backend/web/src/main/resources/modifiedNotification.html b/dmp-backend/web/src/main/resources/modifiedNotification.html new file mode 100644 index 000000000..87460d2da --- /dev/null +++ b/dmp-backend/web/src/main/resources/modifiedNotification.html @@ -0,0 +1,304 @@ + + + + + + Simple Transactional Email + + + + + + + + + +
  +
+ + + This is preheader text. Some clients will show this text as a preview. + + + + + + + + +
+ + + + +
+

Dear {recipient},

+

{reasonName} just made changes to the {name}.

+ + + + + + + +
+ + + + + + +
Click here to view it.
+
+ +
+
+ + + + + + +
+
 
+ + \ No newline at end of file diff --git a/dmp-db-scema/updates/07/02_Add_Notification_Table.sql b/dmp-db-scema/updates/07/02_Add_Notification_Table.sql new file mode 100644 index 000000000..e5d125455 --- /dev/null +++ b/dmp-db-scema/updates/07/02_Add_Notification_Table.sql @@ -0,0 +1,20 @@ +CREATE TABLE public."Notification" ( + id uuid NOT NULL, + "UserId" uuid, + "IsActive" integer NOT NULL, + "Type" integer NOT NULL, + "ContactTypeHint" integer, + "ContactHint" character varying, + "Data" character varying, + "NotifyState" integer NOT NULL, + "NotifiedAt" timestamp without time zone, + "RetryCount" integer, + "CreatedAt" timestamp without time zone, + "UpdatedAt" timestamp without time zone +); + +ALTER TABLE ONLY public."Notification" + ADD CONSTRAINT "Notification_pkey" PRIMARY KEY (id); + +ALTER TABLE ONLY public."Notification" + ADD CONSTRAINT "NotificationUserReference" FOREIGN KEY ("UserId") REFERENCES public."UserInfo"(id);