integrated notificaiton and audit

This commit is contained in:
Timo Bejan 2021-09-15 11:55:39 +03:00
parent 310e526b69
commit e73565aa71
17 changed files with 608 additions and 1 deletions

View File

@ -50,6 +50,18 @@
<groupId>com.iqser.red.commons</groupId>
<artifactId>jackson-commons</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.4.28.Final</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
<version>2.4.5</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,24 @@
package com.iqser.red.service.persistence.service.v1.api.model.data.audit;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.HashMap;
import java.util.Map;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AddNotificationRequest {
private String userId;
private String issuerId;
private String notificationType;
@Builder.Default
private Map<String, Object> target = new HashMap<>();
}

View File

@ -0,0 +1,41 @@
package com.iqser.red.service.persistence.service.v1.api.model.data.audit;
import com.iqser.red.service.persistence.service.v1.api.utils.JSONConverter;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.time.OffsetDateTime;
import java.util.Map;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name="audit")
public class AuditModel {
@Id
@GeneratedValue
private long recordId;
@Column
private OffsetDateTime recordDate;
@Column
private String objectId;
@Column
private String category;
@Column
private String userId;
@Column
private String message;
@Convert(converter = JSONConverter.class)
@Column(columnDefinition = "json")
private Map<String, Object> details;
}

View File

@ -0,0 +1,27 @@
package com.iqser.red.service.persistence.service.v1.api.model.data.audit;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.HashMap;
import java.util.Map;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AuditRequest {
private String objectId;
private String category;
private String userId;
private String message;
@Builder.Default
private Map<String, Object> details = new HashMap<>();
}

View File

@ -0,0 +1,21 @@
package com.iqser.red.service.persistence.service.v1.api.model.data.audit;
import lombok.Data;
import java.time.OffsetDateTime;
@Data
public class AuditSearchRequest {
private String category;
private String userId;
private String objectId;
private String requestingUserId;
private OffsetDateTime from;
private OffsetDateTime to;
private int page;
private int pageSize;
}

View File

@ -0,0 +1,8 @@
package com.iqser.red.service.persistence.service.v1.api.model.data.audit;
public interface CategoryModel {
String getCategory();
long getRecordCount();
}

View File

@ -0,0 +1,53 @@
package com.iqser.red.service.persistence.service.v1.api.model.data.notification;
import com.iqser.red.service.persistence.service.v1.api.utils.JSONConverter;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.TypeDef;
import javax.persistence.*;
import java.time.OffsetDateTime;
import java.util.HashMap;
import java.util.Map;
@Data
@NoArgsConstructor
@Entity
@Table(name = "notification")
public class Notification {
@Id
@GeneratedValue
private long id;
@Column
private String userId;
@Column
private String notificationType;
@Column
private String issuerId;
@Column
private OffsetDateTime creationDate;
@Column
private OffsetDateTime seenDate;
@Column
private OffsetDateTime readDate;
@Column
private OffsetDateTime softDeleted;
@Column
private String notificationDetails;
@Convert(converter = JSONConverter.class)
@Column(columnDefinition = "json")
private Map<String, Object> target = new HashMap<>();
}

View File

@ -0,0 +1,37 @@
package com.iqser.red.service.persistence.service.v1.api.resources;
import com.iqser.red.service.persistence.service.v1.api.model.data.audit.AuditModel;
import com.iqser.red.service.persistence.service.v1.api.model.data.audit.AuditRequest;
import com.iqser.red.service.persistence.service.v1.api.model.data.audit.AuditSearchRequest;
import com.iqser.red.service.persistence.service.v1.api.model.data.audit.CategoryModel;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import java.util.List;
public interface AuditResource {
String PATH = "/audit";
/**
* @param auditRequest - details to audit
* @throws org.springframework.web.server.ResponseStatusException - 404 - Not Found in case the object doesn't exist
*/
@ResponseBody
@ResponseStatus(value = HttpStatus.OK)
@PostMapping(value = PATH, consumes = MediaType.APPLICATION_JSON_VALUE)
void audit(@RequestBody AuditRequest auditRequest);
@ResponseBody
@ResponseStatus(value = HttpStatus.OK)
@PostMapping(value = PATH + "/search", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)
Page<AuditModel> search(@RequestBody AuditSearchRequest auditSearchRequest);
@ResponseBody
@ResponseStatus(value = HttpStatus.OK)
@GetMapping(value = PATH + "/categories", produces = MediaType.APPLICATION_JSON_VALUE)
List<CategoryModel> getCategories();
}

View File

@ -0,0 +1,50 @@
package com.iqser.red.service.persistence.service.v1.api.resources;
import com.iqser.red.service.persistence.service.v1.api.model.data.audit.AddNotificationRequest;
import com.iqser.red.service.persistence.service.v1.api.model.data.notification.Notification;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@ResponseStatus(value = HttpStatus.OK)
public interface NotificationResource {
String NOTIFICATION_PATH = "/notification";
String TOGGLE_SEEN_PATH = "/toggle-seen";
String TOGGLE_READ_PATH = "/toggle-read";
String USER_ID_PARAM = "userId";
String USER_ID_PATH_PARAM = "/{" + USER_ID_PARAM + "}";
String INCLUDE_SEEN_PARAM = "includeSeen";
String SET_SEEN_PARAM = "setSeen";
String SET_READ_PARAM = "setRead";
@PostMapping(value = NOTIFICATION_PATH, consumes = MediaType.APPLICATION_JSON_VALUE)
void addNotification(@RequestBody AddNotificationRequest addNotificationRequest);
@PostMapping(value = NOTIFICATION_PATH + TOGGLE_SEEN_PATH + USER_ID_PATH_PARAM, consumes = MediaType.APPLICATION_JSON_VALUE)
void toggleSeen(@PathVariable(USER_ID_PARAM) String userId, @RequestBody List<Long> notificationIds,
@RequestParam(SET_SEEN_PARAM) boolean setSeen);
@PostMapping(value = NOTIFICATION_PATH + TOGGLE_READ_PATH + USER_ID_PATH_PARAM, consumes = MediaType.APPLICATION_JSON_VALUE)
void toggleRead(@PathVariable(USER_ID_PARAM) String userId, @RequestBody List<Long> notificationIds,
@RequestParam(SET_READ_PARAM) boolean setRead);
@DeleteMapping(value = NOTIFICATION_PATH + USER_ID_PATH_PARAM, consumes = MediaType.APPLICATION_JSON_VALUE)
void softDelete(@PathVariable(USER_ID_PARAM) String userId, @RequestBody List<Long> notificationIds);
@ResponseBody
@ResponseStatus(value = HttpStatus.OK)
@GetMapping(value = NOTIFICATION_PATH + USER_ID_PATH_PARAM, produces = MediaType.APPLICATION_JSON_VALUE)
List<Notification> getNotifications(@PathVariable(USER_ID_PARAM) String userId,
@RequestParam(INCLUDE_SEEN_PARAM) boolean includeSeen);
}

View File

@ -0,0 +1,27 @@
package com.iqser.red.service.persistence.service.v1.api.utils;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
import java.util.Map;
@Converter(autoApply = true)
public class JSONConverter implements AttributeConverter<Map<String, Object>, String> {
private final ObjectMapper objectMapper = new ObjectMapper();
@SneakyThrows
@Override
public String convertToDatabaseColumn(Map<String, Object> map) {
return objectMapper.writeValueAsString(map);
}
@SneakyThrows
@Override
public Map<String, Object> convertToEntityAttribute(String data) {
return objectMapper.readValue(data, Map.class);
}
}

View File

@ -3,9 +3,11 @@ package com.iqser.red.service.persistence.management.v1.processor;
import com.iqser.red.service.persistence.management.v1.processor.client.PDFTronRedactionClient;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.repository.ColorsRepository;
import com.iqser.red.service.persistence.service.v1.api.model.data.annotations.Comment;
import com.iqser.red.service.persistence.service.v1.api.model.data.audit.AuditModel;
import com.iqser.red.service.persistence.service.v1.api.model.data.configuration.Colors;
import com.iqser.red.service.persistence.service.v1.api.model.data.dossier.Dossier;
import com.iqser.red.service.persistence.service.v1.api.model.data.download.DownloadStatus;
import com.iqser.red.service.persistence.service.v1.api.model.data.notification.Notification;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.ComponentScan;
@ -14,7 +16,7 @@ import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@Configuration
@ComponentScan
@EntityScan(basePackageClasses = {Comment.class, Colors.class, Dossier.class, DownloadStatus.class})
@EntityScan(basePackageClasses = {Comment.class, AuditModel.class, Notification.class, Colors.class, Dossier.class, DownloadStatus.class})
@EnableJpaRepositories(basePackageClasses = ColorsRepository.class)
@EnableFeignClients(basePackageClasses = {PDFTronRedactionClient.class})
public class PersistenceServiceProcessorConfiguration {

View File

@ -0,0 +1,85 @@
package com.iqser.red.service.persistence.management.v1.processor.service.persistence;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.repository.AuditRepository;
import com.iqser.red.service.persistence.service.v1.api.model.data.audit.AuditModel;
import com.iqser.red.service.persistence.service.v1.api.model.data.audit.AuditRequest;
import com.iqser.red.service.persistence.service.v1.api.model.data.audit.AuditSearchRequest;
import com.iqser.red.service.persistence.service.v1.api.model.data.audit.CategoryModel;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.beans.BeanUtils;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
import java.time.OffsetDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
@RequiredArgsConstructor
public class AuditPersistenceService {
private final static String AUDIT_LOG_CATEGORY = "AUDIT_LOG";
private final AuditRepository auditRepository;
@SneakyThrows
public void insertRecord(AuditRequest auditRequest) {
var auditModel = new AuditModel();
BeanUtils.copyProperties(auditRequest, auditModel);
auditModel.setRecordDate(OffsetDateTime.now());
auditRepository.save(auditModel);
}
public List<CategoryModel> getCategories() {
return auditRepository.findCategories();
}
public Page<AuditModel> search(AuditSearchRequest auditRequest) {
AuditModel example = new AuditModel();
example.setCategory(auditRequest.getCategory());
example.setUserId(auditRequest.getUserId());
example.setObjectId(auditRequest.getObjectId());
var result = auditRepository.findAll(Example.of(example), PageRequest.of(auditRequest.getPage(), auditRequest.getPageSize()));
// after search, insert a record logging the search
this.insertRecord(AuditRequest.builder()
.category(AUDIT_LOG_CATEGORY)
.message("Audit Log Accessed")
.userId(auditRequest.getRequestingUserId())
.details(searchRequestToMap(auditRequest))
.build());
return result;
}
private Map<String, Object> searchRequestToMap(AuditSearchRequest auditSearchRequest) {
var map = new HashMap<String, Object>();
map.put("userId", auditSearchRequest.getUserId());
map.put("category", auditSearchRequest.getCategory());
map.put("objectId", auditSearchRequest.getObjectId());
map.put("from", auditSearchRequest.getFrom());
map.put("to", auditSearchRequest.getTo());
map.put("page", auditSearchRequest.getPage());
map.put("pageSize", auditSearchRequest.getPageSize());
return map;
}
}

View File

@ -0,0 +1,72 @@
package com.iqser.red.service.persistence.management.v1.processor.service.persistence;
import com.iqser.red.service.persistence.management.v1.processor.exception.NotFoundException;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.repository.NotificationRepository;
import com.iqser.red.service.persistence.service.v1.api.model.data.audit.AddNotificationRequest;
import com.iqser.red.service.persistence.service.v1.api.model.data.notification.Notification;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import java.time.OffsetDateTime;
import java.util.List;
@Service
@RequiredArgsConstructor
public class NotificationPersistenceService {
private final NotificationRepository notificationRepository;
@SneakyThrows
public void insertNotification(AddNotificationRequest addNotificationRequest) {
var notification = new Notification();
BeanUtils.copyProperties(addNotificationRequest, notification);
notification.setCreationDate(OffsetDateTime.now());
notificationRepository.save(notification);
}
public void setSeenDate(String userId, long notificationId, OffsetDateTime seenDate) {
notificationRepository.findByIdAndUserId(notificationId, userId).ifPresentOrElse(notification -> notification.setSeenDate(seenDate), () -> {
throw new NotFoundException("Notification not found");
});
}
public void setReadDate(String userId, long notificationId, OffsetDateTime readDate) {
notificationRepository.findByIdAndUserId(notificationId, userId).ifPresentOrElse(notification -> notification.setReadDate(readDate), () -> {
throw new NotFoundException("Notification not found");
});
}
public void softDelete(String userId, long notificationId) {
notificationRepository.findByIdAndUserId(notificationId, userId).ifPresentOrElse(notification -> notification.setSoftDeleted(OffsetDateTime.now()), () -> {
throw new NotFoundException("Notification not found");
});
}
public void hardDelete(long notificationId) {
notificationRepository.deleteById(notificationId);
}
public List<Notification> getNotifications(String userId, boolean includeSeen) {
if (includeSeen) {
return notificationRepository.findForUser(userId);
} else {
return notificationRepository.findNotSeenForUser(userId);
}
}
}

View File

@ -0,0 +1,19 @@
package com.iqser.red.service.persistence.management.v1.processor.service.persistence.repository;
import com.iqser.red.service.persistence.service.v1.api.model.data.audit.AuditModel;
import com.iqser.red.service.persistence.service.v1.api.model.data.audit.CategoryModel;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;
public interface AuditRepository extends JpaRepository<AuditModel,Long> {
@Query("SELECT a.category, count(a) as recordCount FROM AuditModel a GROUP BY a.category")
List<CategoryModel> findCategories();
Page<AuditModel> findAllByObjectId(double price, Pageable pageable);
}

View File

@ -0,0 +1,21 @@
package com.iqser.red.service.persistence.management.v1.processor.service.persistence.repository;
import com.iqser.red.service.persistence.service.v1.api.model.data.notification.Notification;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;
import java.util.Optional;
public interface NotificationRepository extends JpaRepository<Notification, Long> {
List<Notification> findByUserIdOrderByCreationDateDesc(String userId);
@Query("Select n from Notification n where n.softDeleted is null and n.userId = :userId order by n.creationDate desc")
List<Notification> findForUser(String userId);
@Query("Select n from Notification n where n.seenDate is null and n.softDeleted is null and n.userId = :userId order by n.creationDate desc")
List<Notification> findNotSeenForUser(String userId);
Optional<Notification> findByIdAndUserId(long notificationId, String userId);
}

View File

@ -0,0 +1,38 @@
package com.iqser.red.service.file.management.v1.server.controller;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.AuditPersistenceService;
import com.iqser.red.service.persistence.service.v1.api.model.data.audit.AuditModel;
import com.iqser.red.service.persistence.service.v1.api.model.data.audit.AuditRequest;
import com.iqser.red.service.persistence.service.v1.api.model.data.audit.AuditSearchRequest;
import com.iqser.red.service.persistence.service.v1.api.model.data.audit.CategoryModel;
import com.iqser.red.service.persistence.service.v1.api.resources.AuditResource;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequiredArgsConstructor
public class AuditController implements AuditResource {
private final AuditPersistenceService auditPersistenceService;
@Override
public void audit(@RequestBody AuditRequest auditRequest) {
auditPersistenceService.insertRecord(auditRequest);
}
@Override
public Page<AuditModel> search(@RequestBody AuditSearchRequest auditSearchRequest) {
return auditPersistenceService.search(auditSearchRequest);
}
@Override
public List<CategoryModel> getCategories() {
return auditPersistenceService.getCategories();
}
}

View File

@ -0,0 +1,70 @@
package com.iqser.red.service.file.management.v1.server.controller;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.NotificationPersistenceService;
import com.iqser.red.service.persistence.service.v1.api.model.data.audit.AddNotificationRequest;
import com.iqser.red.service.persistence.service.v1.api.model.data.notification.Notification;
import com.iqser.red.service.persistence.service.v1.api.resources.NotificationResource;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.OffsetDateTime;
import java.util.List;
@RestController
@RequiredArgsConstructor
public class NotificationController implements NotificationResource {
private final NotificationPersistenceService notificationPersistenceService;
public void addNotification(@RequestBody AddNotificationRequest addNotificationRequest) {
notificationPersistenceService.insertNotification(addNotificationRequest);
}
public void toggleSeen(@PathVariable(USER_ID_PARAM) String userId, @RequestBody List<Long> notificationIds,
@RequestParam(SET_SEEN_PARAM) boolean setSeen) {
notificationIds.forEach(notificationId -> {
if (setSeen) {
notificationPersistenceService.setSeenDate(userId, notificationId, OffsetDateTime.now());
} else {
notificationPersistenceService.setSeenDate(userId, notificationId, null);
}
});
}
public void toggleRead(@PathVariable(USER_ID_PARAM) String userId, @RequestBody List<Long> notificationIds,
@RequestParam(SET_READ_PARAM) boolean setRead) {
notificationIds.forEach(notificationId -> {
if (setRead) {
notificationPersistenceService.setReadDate(userId, notificationId, OffsetDateTime.now());
} else {
notificationPersistenceService.setReadDate(userId, notificationId, null);
}
});
}
public void softDelete(@PathVariable(USER_ID_PARAM) String userId, @RequestBody List<Long> notificationIds) {
notificationIds.forEach(notificationId -> {
notificationPersistenceService.softDelete(userId, notificationId);
});
}
public List<Notification> getNotifications(@PathVariable(USER_ID_PARAM) String userId,
@RequestParam(INCLUDE_SEEN_PARAM) boolean includeSeen) {
return notificationPersistenceService.getNotifications(userId,includeSeen);
}
}