diff --git a/pom.xml b/pom.xml index c463be91..02b5fb51 100644 --- a/pom.xml +++ b/pom.xml @@ -245,6 +245,12 @@ reactor-netty + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + 2.15.2 + + diff --git a/src/main/java/net/gepafin/tendermanagement/config/JacksonConfig.java b/src/main/java/net/gepafin/tendermanagement/config/JacksonConfig.java new file mode 100644 index 00000000..51ad3191 --- /dev/null +++ b/src/main/java/net/gepafin/tendermanagement/config/JacksonConfig.java @@ -0,0 +1,20 @@ +package net.gepafin.tendermanagement.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class JacksonConfig { + + @Bean + public ObjectMapper objectMapper() { + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + return mapper; + } +} + diff --git a/src/main/java/net/gepafin/tendermanagement/constants/GepafinConstant.java b/src/main/java/net/gepafin/tendermanagement/constants/GepafinConstant.java index 42853112..5869fc81 100644 --- a/src/main/java/net/gepafin/tendermanagement/constants/GepafinConstant.java +++ b/src/main/java/net/gepafin/tendermanagement/constants/GepafinConstant.java @@ -355,5 +355,10 @@ public class GepafinConstant { public static final String NOTIFICATION_DELETED_SUCCESSFULLY="notification.deleted.successfully"; public static final String NOTIFICATION_UPDATED_SUCCESSFULLY="notification.updated.successfully"; public static final String USER_WITH_COMPANY_NOT_FOUND = "user.with.company.not.found"; + + //action log response + public static final String STATUS_CODE_STRING = "statusCode"; + public static final String GET_STATUS_CODE_STRING = "status"; + public static final String MESSAGE_STRING = "message"; } diff --git a/src/main/java/net/gepafin/tendermanagement/repositories/UserActionsRepository.java b/src/main/java/net/gepafin/tendermanagement/repositories/UserActionsRepository.java index f0163c47..804fdb9b 100644 --- a/src/main/java/net/gepafin/tendermanagement/repositories/UserActionsRepository.java +++ b/src/main/java/net/gepafin/tendermanagement/repositories/UserActionsRepository.java @@ -6,5 +6,5 @@ import org.springframework.stereotype.Repository; @Repository public interface UserActionsRepository extends JpaRepository { - UserActionEntity findUserActionById(Long id); + UserActionEntity findUserActionByIdAndIsDeletedFalse(Long id); } diff --git a/src/main/java/net/gepafin/tendermanagement/util/LoggingUtil.java b/src/main/java/net/gepafin/tendermanagement/util/LoggingUtil.java index e2ab4521..90b7a052 100644 --- a/src/main/java/net/gepafin/tendermanagement/util/LoggingUtil.java +++ b/src/main/java/net/gepafin/tendermanagement/util/LoggingUtil.java @@ -41,6 +41,8 @@ public class LoggingUtil { @Autowired private TokenProvider tokenProvider; + private static final ThreadLocal userActionIdHolder = new ThreadLocal<>(); + public UserActionEntity logUserAction(UserActionRequest userActionRequest) { UserActionEntity userAction = new UserActionEntity(); try { @@ -83,12 +85,22 @@ public class LoggingUtil { userAction.setResponse(response); userActionsRepository.save(userAction); userActionRequest.getRequest().setAttribute(GepafinConstant.USER_ACTION_ID, userAction.getId()); + userActionIdHolder.set(userAction.getId()); } catch (Exception e) { log.error("Error logging user action: {}", e.getMessage(), e); } return userAction; } + public Long getUserActionId() { + return userActionIdHolder.get(); + } + + public void clearUserActionId() { + userActionIdHolder.remove(); + log.info("UserActionId cleared from ThreadLocal"); + } + private String normalizeUrl(String url) { url = url.replaceAll("(? getVersionHistoryLogById(Long id) { return versionHistoryRepository.findVersionHistoryByUserActionId(id); } + + public void updateUserActionWithError(Long userActionId, String errorDetails) { + if (userActionId == null) { + return; + } + + UserActionEntity userAction = userActionsRepository.findUserActionByIdAndIsDeletedFalse(userActionId); + if (userAction != null) { + userAction.setResponse(errorDetails); + userActionsRepository.save(userAction); + } + } + + public void updateUserActionWithResponse(Long userActionId, String response) { + if (userActionId == null) { + return; + } + + UserActionEntity userAction = userActionsRepository.findUserActionByIdAndIsDeletedFalse(userActionId); + if (userAction != null) { + userAction.setResponse(response); + userActionsRepository.save(userAction); + } + } + } diff --git a/src/main/java/net/gepafin/tendermanagement/util/UserActionAspect.java b/src/main/java/net/gepafin/tendermanagement/util/UserActionAspect.java new file mode 100644 index 00000000..0064bb82 --- /dev/null +++ b/src/main/java/net/gepafin/tendermanagement/util/UserActionAspect.java @@ -0,0 +1,148 @@ +package net.gepafin.tendermanagement.util; + +import com.amazonaws.services.alexaforbusiness.model.UnauthorizedException; +import jakarta.persistence.EntityNotFoundException; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import net.gepafin.tendermanagement.constants.GepafinConstant; +import net.gepafin.tendermanagement.web.rest.api.errors.CustomValidationException; +import net.gepafin.tendermanagement.web.rest.api.errors.ResourceNotFoundException; +import net.gepafin.tendermanagement.web.rest.api.errors.Status; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.server.ResponseStatusException; + +import java.lang.reflect.InvocationTargetException; +import java.nio.file.AccessDeniedException; +import java.util.LinkedHashMap; +import java.util.Map; + +@Aspect +@Component +@Slf4j +public class UserActionAspect { + + @Autowired + private LoggingUtil loggingUtil; + + @Around("execution(public * net.gepafin.tendermanagement.web.rest.api.impl..*(..))") + public Object logApiResponse(ProceedingJoinPoint joinPoint) throws Throwable { + + Object result; + + HttpServletRequest request = getRequestFromContext(); + try { + Long userActionId = getUserActionIdFromRequest(request); + + if (userActionId != null) { + request.setAttribute(GepafinConstant.USER_ACTION_ID, userActionId); + log.info("Stored userActionId in RequestContext: {}", userActionId); + } else { + userActionId = loggingUtil.getUserActionId(); + if (userActionId != null) { + request.setAttribute(GepafinConstant.USER_ACTION_ID, userActionId); + } + } + + result = joinPoint.proceed(); + + if (result instanceof ResponseEntity) { + Long storedUserActionId = (Long) request.getAttribute(GepafinConstant.USER_ACTION_ID); + handleSuccessResponse((ResponseEntity) result, storedUserActionId == null ? userActionId : storedUserActionId); + } + } catch (Exception ex) { + log.error("Exception occurred: ", ex); + handleError(ex, getUserActionIdFromRequest(request)); + throw ex; + } finally { + loggingUtil.clearUserActionId(); + } + + return result; + } + + private void handleSuccessResponse(ResponseEntity responseEntity, Long userActionId) { + + if (userActionId != null) { + Map responseWithUserAction = new LinkedHashMap<>(); + responseWithUserAction.put(GepafinConstant.STATUS_CODE_STRING, responseEntity.getStatusCode().value()); + + // Log and update user action + loggingUtil.updateUserActionWithResponse(userActionId, Utils.convertMapIntoJsonString(responseWithUserAction)); + log.info("Updated userActionId with response: {}", userActionId); + } + } + + private void handleError(Throwable ex, Long userActionId) throws InvocationTargetException, NoSuchMethodException, IllegalAccessException { + + HttpStatus status = getStatusCodeFromException(ex); + log.info("Status Code received from exception : {}", status); + String errorMessage = ex.getMessage(); + + Map errorResponse = new LinkedHashMap<>(); + errorResponse.put(GepafinConstant.STATUS_CODE_STRING, status.value()); + errorResponse.put(GepafinConstant.GET_STATUS_CODE_STRING, status); + errorResponse.put(GepafinConstant.MESSAGE_STRING, errorMessage); + + if (userActionId != null) { + String errorDetails = Utils.convertMapIntoJsonString(errorResponse); + loggingUtil.updateUserActionWithError(userActionId, errorDetails); + log.info("Updated userActionId with error details: {}", userActionId); + } + } + + private HttpServletRequest getRequestFromContext() { + + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + return attributes != null ? attributes.getRequest() : null; + } + + private Long getUserActionIdFromRequest(HttpServletRequest request) { + + if (request != null) { + Object userActionIdAttr = request.getAttribute(GepafinConstant.USER_ACTION_ID); + return userActionIdAttr != null ? Long.valueOf(userActionIdAttr.toString()) : null; + } + return null; + } + + private HttpStatus getStatusCodeFromException(Throwable ex) { + + if (ex instanceof ResourceNotFoundException) { + return HttpStatus.NOT_FOUND; + } + + if (ex instanceof ResponseStatusException responseStatusException) { + return (HttpStatus) responseStatusException.getStatusCode(); + } + + if (ex instanceof CustomValidationException) { + return HttpStatus.BAD_REQUEST; + } + + if (ex instanceof EntityNotFoundException) { + return HttpStatus.NOT_FOUND; + } + if (ex instanceof IllegalArgumentException || ex instanceof MissingServletRequestParameterException || ex instanceof MethodArgumentNotValidException) { + return HttpStatus.BAD_REQUEST; + } + if (ex instanceof AccessDeniedException) { + return HttpStatus.FORBIDDEN; + } + if (ex instanceof UnauthorizedException) { + return HttpStatus.UNAUTHORIZED; + } + + return HttpStatus.INTERNAL_SERVER_ERROR; + } +} diff --git a/src/main/java/net/gepafin/tendermanagement/util/Utils.java b/src/main/java/net/gepafin/tendermanagement/util/Utils.java index 40f95e3b..0770bce5 100644 --- a/src/main/java/net/gepafin/tendermanagement/util/Utils.java +++ b/src/main/java/net/gepafin/tendermanagement/util/Utils.java @@ -154,6 +154,9 @@ public class Utils { public static String convertMapIntoJsonString(Map map) { try { ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + mapper.enable(SerializationFeature.INDENT_OUTPUT); if (MapUtils.isNotEmpty(map)) { return mapper.writeValueAsString(map); }