Skip to content

Commit

Permalink
Add activity log (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
haiphucnguyen authored Nov 27, 2024
1 parent 8c7f51e commit 3b99a15
Show file tree
Hide file tree
Showing 44 changed files with 1,108 additions and 292 deletions.
15 changes: 15 additions & 0 deletions server/src/main/java/io/flexwork/config/AsyncConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
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
Expand Down Expand Up @@ -41,6 +43,19 @@ public Executor getAsyncExecutor() {
return new ExceptionHandlingAsyncTaskExecutor(executor);
}

@Bean(name = "auditLogExecutor")
public TaskExecutor auditLogaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("SecurityContextAsync-");
executor.initialize();

// Wrap the executor with DelegatingSecurityContextTaskExecutor
return new DelegatingSecurityContextTaskExecutor(executor);
}

@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new SimpleAsyncUncaughtExceptionHandler();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package io.flexwork.modules.audit;

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<>();

/**
* 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) {
fieldHandlers.put(fieldName, handler);
}

/**
* Get the handler for a specific field.
*
* @param fieldName The name of the field.
* @return A handler function that processes the old and new values of the field.
*/
@Override
public BiFunction<Object, Object, String> getHandler(String fieldName) {
return fieldHandlers.get(fieldName);
}

/**
* Initialize field handlers for this entity. Subclasses must define their field-specific logic
* in this method.
*/
protected abstract void initializeFieldHandlers();

/** Constructor to ensure field handlers are initialized in subclasses. */
public AbstractEntityFieldHandlerRegistry() {
initializeFieldHandlers();
}

/**
* Subclasses must provide the entity's class.
*
* @return The class of the entity this registry handles.
*/
@Override
public abstract Class<?> getEntityClass();

/**
* Subclasses must provide the entity's EntityType.
*
* @return The EntityType of the entity.
*/
@Override
public abstract EntityType getEntityType();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.flexwork.modules.audit;

import static j2html.TagCreator.*;

import j2html.tags.DomContent;
import java.util.List;

public class ActivityLogUtils {

public static String generateHtmlLog(List<AuditUtils.FieldChange> changes) {
DomContent htmlContent =
table().with(
thead(tr(th("Field"), th("Old Value"), th("New Value"))),
tbody(
each(
changes,
change ->
tr(
td(change.getFieldName()),
td(
change.getOldValue() != null
? change.getOldValue()
.toString()
: "N/A"),
td(
change.getNewValue() != null
? change.getNewValue()
.toString()
: "N/A")))));

return htmlContent.render();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.flexwork.modules.audit;

import lombok.Getter;
import org.springframework.context.ApplicationEvent;

@Getter
public class AuditLogUpdateEvent extends ApplicationEvent {

private final Object updatedEntity;

public AuditLogUpdateEvent(Object source, Object updatedEntity) {
super(source);
this.updatedEntity = updatedEntity;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package io.flexwork.modules.audit;

import io.flexwork.modules.collab.domain.ActivityLog;
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;

@Component
public class AuditLogUpdateEventListener {

private static final Logger LOG = LoggerFactory.getLogger(AuditLogUpdateEventListener.class);

private final ActivityLogRepository activityLogRepository;
private final EntityFieldHandlerRegistryFactory registryFactory;
private final ApplicationContext applicationContext;

public AuditLogUpdateEventListener(
ActivityLogRepository activityLogRepository,
EntityFieldHandlerRegistryFactory registryFactory,
ApplicationContext applicationContext) {
this.activityLogRepository = activityLogRepository;
this.registryFactory = registryFactory;
this.applicationContext = applicationContext;
}

@Async("auditLogExecutor")
@Transactional
@EventListener
public void onNewTeamRequestCreated(AuditLogUpdateEvent event) {
try {

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);

if (!changes.isEmpty()) {
// Generate HTML content
String htmlLog = ActivityLogUtils.generateHtmlLog(changes);

// Save the aggregated activity log
saveActivityLog(registry.getEntityType(), entityId, htmlLog);
}
} catch (Exception e) {
// Log the exception
LOG.error("Error in async logEntityChanges", e);
}
}

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);
activityLog.setEntityId(entityId);
activityLog.setContent(activityDetails);
activityLog.setCreatedBy(SecurityUtils.getCurrentUserAuditorLogin());
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");
idField.setAccessible(true);
return (Long) idField.get(entity);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException("Failed to extract entity ID", e);
}
}
}
Loading

0 comments on commit 3b99a15

Please sign in to comment.