Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ticket dashboard improvements #18

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package io.flexwork.config;

import java.util.concurrent.Executor;
import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
Expand All @@ -13,15 +12,12 @@
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.security.task.DelegatingSecurityContextTaskExecutor;
import tech.jhipster.async.ExceptionHandlingAsyncTaskExecutor;

@Configuration
@EnableAsync
@EnableScheduling
@EnableSchedulerLock(defaultLockAtMostFor = "10m")
@Profile("!testdev & !testprod")
public class AsyncConfiguration implements AsyncConfigurer {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.flexwork.config;

import javax.sql.DataSource;
import net.javacrumbs.shedlock.core.LockProvider;
import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider;
import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Profile;
import org.springframework.scheduling.annotation.EnableScheduling;

@EnableScheduling
@EnableSchedulerLock(defaultLockAtMostFor = "10m")
@Profile("!testdev & !testprod")
public class SchedulerConfiguration {

@Bean
public LockProvider lockProvider(DataSource dataSource) {
return new JdbcTemplateLockProvider(dataSource);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.flexwork.modules.audit;

import java.time.Instant;
import lombok.Data;

@Data
public abstract class AbstractAuditDTO {
Instant createdAt;
Instant modifiedAt;
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
package io.flexwork.modules.usermanagement.domain;
package io.flexwork.modules.audit;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import io.flexwork.modules.usermanagement.domain.User;
import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.MappedSuperclass;
import java.io.Serializable;
import java.time.Instant;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
Expand All @@ -18,9 +24,11 @@
* by, last modified by attributes.
*/
@MappedSuperclass
@SuperBuilder
@NoArgsConstructor
@EntityListeners(AuditingEntityListener.class)
@JsonIgnoreProperties(
value = {"createdBy", "createdDate", "lastModifiedBy", "lastModifiedDate"},
value = {"createdBy", "createdAt", "modifiedBy", "modifiedAt"},
allowGetters = true)
@Data
public abstract class AbstractAuditingEntity<T> implements Serializable {
Expand All @@ -30,18 +38,26 @@ public abstract class AbstractAuditingEntity<T> implements Serializable {
public abstract T getId();

@CreatedBy
@Column(name = "created_by", nullable = false, length = 256, updatable = false)
private String createdBy;
@Column(name = "created_by", updatable = false)
private Long createdBy;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "created_by", insertable = false, updatable = false)
private User createdByUser;

@CreatedDate
@Column(name = "created_date", updatable = false)
private Instant createdDate = Instant.now();
@Column(name = "created_at", updatable = false)
private Instant createdAt = Instant.now();

@LastModifiedBy
@Column(name = "last_modified_by", length = 256)
private String lastModifiedBy;
@Column(name = "modified_by")
private Long modifiedBy;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "modified_by", insertable = false, updatable = false)
private User modifiedByUser;

@LastModifiedDate
@Column(name = "last_modified_date")
private Instant lastModifiedDate = Instant.now();
@Column(name = "modified_at")
private Instant modifiedAt = Instant.now();
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,18 @@
import io.flexwork.modules.collab.domain.EntityType;
import java.util.HashMap;
import java.util.Map;
import java.util.function.BiFunction;

public abstract class AbstractEntityFieldHandlerRegistry implements EntityFieldHandlerRegistry {

private final Map<String, BiFunction<Object, Object, String>> fieldHandlers = new HashMap<>();
private final Map<String, EntityFieldHandler> fieldHandlers = new HashMap<>();

/**
* Add a field handler for a specific field.
*
* @param fieldName The name of the field.
* @param handler A function that processes the old and new values of the field.
*/
protected void addFieldHandler(String fieldName, BiFunction<Object, Object, String> handler) {
protected void addFieldHandler(String fieldName, EntityFieldHandler handler) {
fieldHandlers.put(fieldName, handler);
}

Expand All @@ -26,7 +25,7 @@ protected void addFieldHandler(String fieldName, BiFunction<Object, Object, Stri
* @return A handler function that processes the old and new values of the field.
*/
@Override
public BiFunction<Object, Object, String> getHandler(String fieldName) {
public EntityFieldHandler getHandler(String fieldName) {
return fieldHandlers.get(fieldName);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
@Getter
public class AuditLogUpdateEvent extends ApplicationEvent {

private final Object previousEntity;
private final Object updatedEntity;

public AuditLogUpdateEvent(Object source, Object updatedEntity) {
public AuditLogUpdateEvent(Object source, Object previousEntity, Object updatedEntity) {
super(source);
this.previousEntity = previousEntity;
this.updatedEntity = updatedEntity;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,12 @@
import io.flexwork.modules.collab.domain.EntityType;
import io.flexwork.modules.collab.repository.ActivityLogRepository;
import io.flexwork.security.SecurityUtils;
import jakarta.persistence.EntityNotFoundException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.event.EventListener;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
Expand Down Expand Up @@ -40,23 +37,17 @@ public AuditLogUpdateEventListener(
@EventListener
public void onNewTeamRequestCreated(AuditLogUpdateEvent event) {
try {

Object previousEntity = event.getPreviousEntity();
Object updatedEntity = event.getUpdatedEntity();
Class<?> entityClass = updatedEntity.getClass();
Long entityId = extractEntityId(updatedEntity);

// Fetch the existing entity
Object existingEntity = fetchExistingEntity(entityClass, entityId);

// Convert existing entity to DTO form using the mapper
Object existingEntityDto = mapEntityToDto(existingEntity.getClass(), existingEntity);

// Get the registry for the entity
EntityFieldHandlerRegistry registry = registryFactory.getRegistry(entityClass);

// Find changes between the existing DTO and updated entity
List<AuditUtils.FieldChange> changes =
AuditUtils.findChanges(existingEntityDto, updatedEntity, registry);
AuditUtils.findChanges(previousEntity, updatedEntity, registry);

if (!changes.isEmpty()) {
// Generate HTML content
Expand All @@ -71,43 +62,6 @@ public void onNewTeamRequestCreated(AuditLogUpdateEvent event) {
}
}

private Object mapEntityToDto(Class<?> entityClass, Object entity) {
try {
// Derive the mapper bean name based on conventions (e.g., "teamMapper")
String mapperBeanName = getMapperBeanName(entityClass);

// Fetch the mapper bean from the Spring context
Object mapper = applicationContext.getBean(mapperBeanName);

// Find the "toDto" method in the mapper
Method toDtoMethod = mapper.getClass().getDeclaredMethod("toDto", entityClass);

// Invoke the "toDto" method and return the result
return toDtoMethod.invoke(mapper, entity);
} catch (Exception e) {
throw new RuntimeException(
"Failed to map entity to DTO for class: " + entityClass.getSimpleName(), e);
}
}

private String getMapperBeanName(Class<?> entityClass) {
// Get the simple name of the entity class
String entityName = entityClass.getSimpleName();

// Remove the "DTO" suffix if it exists
if (entityName.endsWith("DTO")) {
entityName = entityName.substring(0, entityName.length() - 3);
}

// Convert the entity name to the mapper bean name
String mapperBeanName =
Character.toLowerCase(entityName.charAt(0))
+ entityName.substring(1)
+ "MapperImpl";

return mapperBeanName;
}

private void saveActivityLog(EntityType entityType, Long entityId, String activityDetails) {
ActivityLog activityLog = new ActivityLog();
activityLog.setEntityType(entityType);
Expand All @@ -117,46 +71,6 @@ private void saveActivityLog(EntityType entityType, Long entityId, String activi
activityLogRepository.save(activityLog);
}

private Object fetchExistingEntity(Class<?> entityClass, Long entityId) {
// Get the repository bean name for the entity
String repositoryBeanName = getRepositoryBeanName(entityClass);

// Fetch the repository dynamically
JpaRepository<?, Long> repository =
(JpaRepository<?, Long>) applicationContext.getBean(repositoryBeanName);

// Query the existing entity by ID
return repository
.findById(entityId)
.orElseThrow(
() ->
new EntityNotFoundException(
"Entity of class "
+ entityClass.getSimpleName()
+ " with ID "
+ entityId
+ " not found"));
}

private String getRepositoryBeanName(Class<?> entityClass) {
// Get the simple name of the entity class
String entityName = entityClass.getSimpleName();

// Remove "DTO" suffix if present
if (entityName.endsWith("DTO")) {
entityName = entityName.substring(0, entityName.length() - 3);
}

// Convert the entity name to the repository bean name
// Convention: repositoryBeanName = entityName (lowerCamelCase) + "Repository"
String repositoryBeanName =
Character.toLowerCase(entityName.charAt(0))
+ entityName.substring(1)
+ "Repository";

return repositoryBeanName;
}

private Long extractEntityId(Object entity) {
try {
Field idField = entity.getClass().getDeclaredField("id");
Expand Down
41 changes: 6 additions & 35 deletions server/src/main/java/io/flexwork/modules/audit/AuditUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiFunction;

public class AuditUtils {

Expand Down Expand Up @@ -46,54 +45,30 @@ public static List<FieldChange> findChanges(
// Compare values and check for changes
if ((oldValue == null && newValue != null)
|| (oldValue != null && !oldValue.equals(newValue))) {
BiFunction<Object, Object, String> handler = registry.getHandler(field.getName());
String description;
EntityFieldHandler handler = registry.getHandler(field.getName());

// Use custom handler if available, otherwise provide a default change description
// Only add the fields to audit log if the handler is presented
if (handler != null) {
description = handler.apply(oldValue, newValue);
} else {
description =
generateDefaultChangeDescription(field.getName(), oldValue, newValue);
oldValue = handler.getFieldGetter().apply(oldEntity, oldValue);
newValue = handler.getFieldGetter().apply(newEntity, newValue);
changes.add(new FieldChange(handler.getFieldName(), oldValue, newValue));
}

changes.add(new FieldChange(field.getName(), oldValue, newValue, description));
}
}

return changes;
}

/**
* Generate a default description for a field change when no custom handler is available.
*
* @param fieldName The name of the field.
* @param oldValue The old value of the field.
* @param newValue The new value of the field.
* @return A default string description of the field change.
*/
private static String generateDefaultChangeDescription(
String fieldName, Object oldValue, Object newValue) {
return fieldName
+ " changed from '"
+ (oldValue != null ? oldValue : "N/A")
+ "' to '"
+ (newValue != null ? newValue : "N/A")
+ "'";
}

/** Represents a single field change in the audit log. */
public static class FieldChange {
private final String fieldName;
private final Object oldValue;
private final Object newValue;
private final String description;

public FieldChange(String fieldName, Object oldValue, Object newValue, String description) {
public FieldChange(String fieldName, Object oldValue, Object newValue) {
this.fieldName = fieldName;
this.oldValue = oldValue;
this.newValue = newValue;
this.description = description;
}

public String getFieldName() {
Expand All @@ -107,9 +82,5 @@ public Object getOldValue() {
public Object getNewValue() {
return newValue;
}

public String getDescription() {
return description;
}
}
}
Loading
Loading