Skip to content

Commit

Permalink
Merge pull request #64 from nikanique/feat/custom-deserialization
Browse files Browse the repository at this point in the history
Feat/custom deserialization and isRequired and defaultValue
  • Loading branch information
birddevelper authored Jan 12, 2025
2 parents 7500fb4 + ee5049f commit 883fc30
Show file tree
Hide file tree
Showing 11 changed files with 118 additions and 28 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
</parent>
<groupId>io.github.nikanique</groupId>
<artifactId>spring-rest-framework</artifactId>
<version>2.2.0</version>
<version>2.3.0</version>
<packaging>jar</packaging>
<name>spring-rest-framework</name>
<description>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,8 @@
String methodName() default "not-provided";

String source() default "not-provided";

String defaultValue() default "not-provided";

boolean isRequired() default false;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import io.github.nikanique.springrestframework.annotation.FieldValidation;
import io.github.nikanique.springrestframework.exceptions.ValidationException;
import jakarta.persistence.EntityManager;

import java.lang.invoke.MethodHandle;
import java.text.ParseException;
Expand Down Expand Up @@ -104,4 +105,7 @@ private void validateDateField(java.util.Date value, String fieldName, FieldVali
}
}

public void postDeserialization(EntityManager entityManager) {
// Default implementation
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,15 @@ public static Map<String, FieldMetadata> getDtoByClassName(Class<?> clazz) {
return Stream.of(cls.getDeclaredFields())
.map(field -> {
MethodHandle getterMethodHandle = null;
MethodHandle setterMethodHandle = null;
try {
String getterName = "get" + Character.toUpperCase(field.getName().charAt(0)) + field.getName().substring(1);
MethodType methodType = MethodType.methodType(field.getType());
getterMethodHandle = lookup.findVirtual(cls, getterName, methodType);
String setterName = "set" + Character.toUpperCase(field.getName().charAt(0)) + field.getName().substring(1);
MethodType getterMethodType = MethodType.methodType(field.getType());
MethodType setterMethodType = MethodType.methodType(void.class, field.getType());
getterMethodHandle = lookup.findVirtual(cls, getterName, getterMethodType);
setterMethodHandle = lookup.findVirtual(cls, setterName, setterMethodType);

} catch (NoSuchMethodException | IllegalAccessException e) {
// Log the exception or handle it as per your requirement
}
Expand All @@ -35,7 +40,8 @@ public static Map<String, FieldMetadata> getDtoByClassName(Class<?> clazz) {
field.getAnnotation(ReadOnly.class),
field.getAnnotation(WriteOnly.class),
field.getAnnotation(ReferencedModel.class),
getterMethodHandle);
getterMethodHandle,
setterMethodHandle);
})
.collect(Collectors.toMap(fieldMetadata -> fieldMetadata.getField().getName(), fieldMetadata -> fieldMetadata));
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ public class FieldMetadata {
private final WriteOnly writeOnly;
private final ReferencedModel referencedModel;
private final MethodHandle getterMethodHandle;
private final MethodHandle setterMethodHandle;

public FieldMetadata(Field field, Class<?> fieldType, FieldValidation validation, Expose expose, ReadOnly readOnly, WriteOnly writeOnly, ReferencedModel referencedModel, MethodHandle getterMethodHandle) {
public FieldMetadata(Field field, Class<?> fieldType, FieldValidation validation, Expose expose, ReadOnly readOnly, WriteOnly writeOnly, ReferencedModel referencedModel, MethodHandle getterMethodHandle, MethodHandle setterMethodHandle) {
this.field = field;
this.fieldType = fieldType;
this.validation = validation;
Expand All @@ -27,5 +28,6 @@ public FieldMetadata(Field field, Class<?> fieldType, FieldValidation validation
this.writeOnly = writeOnly;
this.referencedModel = referencedModel;
this.getterMethodHandle = getterMethodHandle;
this.setterMethodHandle = setterMethodHandle;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,14 @@ public Model fromDto(Object dto, Class<?> dtoClass) {


// Create an instance of the main entity class
Model entity = this.entityClass.newInstance();
Model entity = this.entityClass.getDeclaredConstructor().newInstance();
BeanWrapper entityWrapper = new BeanWrapperImpl(entity);

List<String> ignoreProperties = new ArrayList<>();
for (String fieldName : fieldMetadata.keySet()) {
Object fieldValue = fieldMetadata.get(fieldName).getGetterMethodHandle().invoke(dto);
ReadOnly readOnlyAnnotation = fieldMetadata.get(fieldName).getReadOnly();
if (readOnlyAnnotation != null) {
if (readOnlyAnnotation != null && fieldValue == null) {
ignoreProperties.add(fieldName);
continue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,25 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.github.nikanique.springrestframework.annotation.Expose;
import io.github.nikanique.springrestframework.annotation.ReadOnly;
import io.github.nikanique.springrestframework.common.FieldType;
import io.github.nikanique.springrestframework.dto.DtoManager;
import io.github.nikanique.springrestframework.dto.FieldMetadata;
import io.github.nikanique.springrestframework.exceptions.BadRequestException;
import io.github.nikanique.springrestframework.utilities.MethodReflectionHelper;
import io.github.nikanique.springrestframework.utilities.StringUtils;
import io.github.nikanique.springrestframework.utilities.ValueFormatter;
import jakarta.persistence.EntityManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Method;
import java.sql.Timestamp;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
Expand All @@ -28,21 +34,23 @@
public class Serializer {

private final ObjectMapper objectMapper;
private final EntityManager entityManager;

@Autowired
public Serializer(ObjectMapper objectMapper) {
public Serializer(ObjectMapper objectMapper, EntityManager entityManager) {
this.objectMapper = objectMapper;
this.entityManager = entityManager;
}

public Object deserialize(String requestBody, Class<?> dtoClass) throws IOException {
public Object deserialize(String requestBody, Class<?> dtoClass) throws Throwable {
return deserialize(requestBody, dtoClass, false, false);
}

public Object deserialize(String requestBody, Class<?> dtoClass, Boolean raiseValidationError) throws IOException {
public Object deserialize(String requestBody, Class<?> dtoClass, Boolean raiseValidationError) throws Throwable {
return deserialize(requestBody, dtoClass, raiseValidationError, false);
}

public Object deserialize(String requestBody, Class<?> dtoClass, Boolean raiseValidationError, Boolean partial) throws IOException {
public Object deserialize(String requestBody, Class<?> dtoClass, Boolean raiseValidationError, Boolean partial) throws Throwable {
Set<String> fieldNames = new HashSet<>();
if (partial) {
fieldNames = getPresentFields(requestBody);
Expand All @@ -51,17 +59,76 @@ public Object deserialize(String requestBody, Class<?> dtoClass, Boolean raiseVa

}

public Object deserialize(String requestBody, Class<?> dtoClass, Boolean raiseValidationError, Set<String> fields) throws IOException {
Object dto = objectMapper.readValue(requestBody, dtoClass);
public Object deserialize(String requestBody, Class<?> dtoClass, Boolean raiseValidationError, Set<String> fields) throws Throwable {
Object dto = generateDTO(requestBody, dtoClass);
if (!fields.isEmpty()) {
invokeValidateIfExists(dto, dtoClass, raiseValidationError, fields);
} else {
invokeValidateIfExists(dto, dtoClass, raiseValidationError);
}

invokePostDeserialization(dto, dtoClass, entityManager);
return dtoClass.cast(dto);
}

private Object generateDTO(String requestBody, Class<?> dtoClass) throws Throwable {
Object dto = objectMapper.readValue(requestBody, dtoClass);
Map<String, FieldMetadata> fieldMetadata = DtoManager.getDtoByClassName(dtoClass);
for (String fieldName : fieldMetadata.keySet()) {
ReadOnly readOnlyAnnotation = fieldMetadata.get(fieldName).getReadOnly();
Expose exposeAnnotation = fieldMetadata.get(fieldName).getExpose();
Object fieldValue = fieldMetadata.get(fieldName).getGetterMethodHandle().invoke(dto);

// Read only field set to null
if (readOnlyAnnotation != null && fieldValue != null) {
try {
fieldMetadata.get(fieldName).getSetterMethodHandle().invoke(dto, null);
} catch (Throwable e) {
log.error("{} setter method invocation failed.", fieldName);
}
}


// Default value
if (readOnlyAnnotation == null && exposeAnnotation != null && fieldValue == null &&
!exposeAnnotation.defaultValue().equals("not-provided")) {
Class<?> fieldType = fieldMetadata.get(fieldName).getFieldType();
String defaultValue = exposeAnnotation.defaultValue();
try {
if (fieldType == Integer.class || fieldType == int.class) {
fieldMetadata.get(fieldName).getSetterMethodHandle().invoke(dto, Integer.valueOf(defaultValue));
} else if (fieldType == Long.class || fieldType == long.class) {
fieldMetadata.get(fieldName).getSetterMethodHandle().invoke(dto, Long.valueOf(defaultValue));
} else if (fieldType == Double.class || fieldType == double.class) {
fieldMetadata.get(fieldName).getSetterMethodHandle().invoke(dto, Double.valueOf(defaultValue));
} else if (fieldType == Float.class || fieldType == float.class) {
fieldMetadata.get(fieldName).getSetterMethodHandle().invoke(dto, Float.valueOf(defaultValue));
} else if (fieldType == LocalDate.class) {
fieldMetadata.get(fieldName).getSetterMethodHandle().invoke(dto, LocalDate.parse(defaultValue));
} else if (fieldType == Date.class) {
fieldMetadata.get(fieldName).getSetterMethodHandle().invoke(dto, java.sql.Date.valueOf(defaultValue));
} else if (fieldType == Timestamp.class) {
fieldMetadata.get(fieldName).getSetterMethodHandle().invoke(dto, Timestamp.valueOf(defaultValue));
} else if (fieldType == String.class) {
fieldMetadata.get(fieldName).getSetterMethodHandle().invoke(dto, defaultValue);
} else if (fieldType == Boolean.class || fieldType == boolean.class) {
fieldMetadata.get(fieldName).getSetterMethodHandle().invoke(dto, Boolean.valueOf(defaultValue));
} else {
log.error("{} field type is not supported for default value.", fieldName);
}
} catch (Throwable e) {
log.error("{} default value could not be set.", fieldName);
}
}

// Required check

if (exposeAnnotation != null && fieldValue == null && exposeAnnotation.isRequired()) {
throw new BadRequestException(fieldName, "Field is required.");
}
}
return dto;
}

public Set<String> getPresentFields(String requestBody) throws JsonProcessingException {
JsonNode requestBodyNode = objectMapper.readTree(requestBody);
Set<String> fieldNames = new HashSet<>();
Expand Down Expand Up @@ -98,6 +165,20 @@ private void invokeValidateIfExists(Object dto, Class<?> dtoClass, Boolean raise
}
}

private void invokePostDeserialization(Object dto, Class<?> dtoClass, EntityManager entityManager) {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType methodType = MethodType.methodType(void.class, EntityManager.class);
try {
MethodHandle validateMethodHandle = lookup.findVirtual(dtoClass, "postDeserialization", methodType);
validateMethodHandle.invoke(dto, entityManager);
} catch (NoSuchMethodException | IllegalAccessException e) {
log.error("postDeserialization method does not exist or is not accessible");
} catch (Throwable t) {
throw new RuntimeException("postDeserialization method failed", t);
}
}


public ObjectNode serialize(Object object, SerializerConfig serializerConfig) {
ObjectNode serializedData = serializeObject(object, serializerConfig.getFields(), "");
if (serializerConfig.getToRepresentMethod() != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,13 @@ public abstract class BaseGenericController<Model, ID, ModelRepository extends J
implements ApplicationContextAware {

final protected ModelRepository repository;

private final Map<String, List<String>> endpointsRequiredAuthorities;
@Getter
protected Serializer serializer;
protected ApplicationContext context;
private Map<String, List<String>> endpointsRequiredAuthorities;

@Autowired
public BaseGenericController(@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") ModelRepository repository) {
public BaseGenericController(ModelRepository repository) {
this.repository = repository;
this.endpointsRequiredAuthorities = new HashMap<>();
this.configRequiredAuthorities(this.endpointsRequiredAuthorities);
Expand Down Expand Up @@ -68,7 +67,7 @@ public void setApplicationContext(ApplicationContext applicationContext) throws
* }
* </pre>
*
* @return
* @return The class type of the DTO
*/
protected abstract Class<?> getDTO();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

import java.io.IOException;

/**
* This interface provides methods for creating entities.
*
Expand All @@ -39,7 +37,7 @@ default SerializerConfig configCreateSerializerFields() {
}


default ResponseEntity<ObjectNode> create(BaseGenericController controller, HttpServletRequest request) throws IOException {
default ResponseEntity<ObjectNode> create(BaseGenericController controller, HttpServletRequest request) throws Throwable {
String requestBody = this.getRequestBody(request);
Object dto = controller.getSerializer().deserialize(requestBody, getCreateRequestBodyDTO(), true);
Model entity = this.getEntityHelper().fromDto(dto, this.getCreateRequestBodyDTO());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@
import org.springframework.web.bind.annotation.*;
import org.springframework.web.method.HandlerMethod;

import java.io.IOException;

@Getter
public abstract class GenericCommandController<Model, ID, ModelRepository extends JpaRepository<Model, ID> & JpaSpecificationExecutor<Model>>
extends BaseGenericController<Model, ID, ModelRepository>
Expand Down Expand Up @@ -81,7 +79,7 @@ protected Filter configLookupFilter() {
}

@PostMapping("/")
public ResponseEntity<ObjectNode> post(HttpServletRequest request) throws IOException {
public ResponseEntity<ObjectNode> post(HttpServletRequest request) throws Throwable {
this.authorizeRequest(request);
return this.create(this, request);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.method.HandlerMethod;

import java.io.IOException;

/**
* The GenericCreateController class is a generic controller designed for use in Spring Boot applications for creating model's records.
* It provides a common implementation for creating records. It exposes endpoint with POST method.
Expand Down Expand Up @@ -73,7 +71,7 @@ public Class<?> getCreateResponseBodyDTO() {
}

@PostMapping("/")
public ResponseEntity<ObjectNode> post(HttpServletRequest request) throws IOException {
public ResponseEntity<ObjectNode> post(HttpServletRequest request) throws Throwable {
this.authorizeRequest(request);
return this.create(this, request);
}
Expand Down

0 comments on commit 883fc30

Please sign in to comment.