-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
8c7f51e
commit 3b99a15
Showing
44 changed files
with
1,108 additions
and
292 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
59 changes: 59 additions & 0 deletions
59
server/src/main/java/io/flexwork/modules/audit/AbstractEntityFieldHandlerRegistry.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} |
33 changes: 33 additions & 0 deletions
33
server/src/main/java/io/flexwork/modules/audit/ActivityLogUtils.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
15 changes: 15 additions & 0 deletions
15
server/src/main/java/io/flexwork/modules/audit/AuditLogUpdateEvent.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
169 changes: 169 additions & 0 deletions
169
server/src/main/java/io/flexwork/modules/audit/AuditLogUpdateEventListener.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
Oops, something went wrong.