diff --git a/pom.xml b/pom.xml
index b398ece..04d5131 100644
--- a/pom.xml
+++ b/pom.xml
@@ -10,7 +10,7 @@
io.github.nikanique
spring-rest-framework
- 2.2.0
+ 2.3.0
jar
spring-rest-framework
diff --git a/src/main/java/io/github/nikanique/springrestframework/annotation/Expose.java b/src/main/java/io/github/nikanique/springrestframework/annotation/Expose.java
index 7c44dd4..d6c86d4 100644
--- a/src/main/java/io/github/nikanique/springrestframework/annotation/Expose.java
+++ b/src/main/java/io/github/nikanique/springrestframework/annotation/Expose.java
@@ -15,4 +15,8 @@
String methodName() default "not-provided";
String source() default "not-provided";
+
+ String defaultValue() default "not-provided";
+
+ boolean isRequired() default false;
}
\ No newline at end of file
diff --git a/src/main/java/io/github/nikanique/springrestframework/dto/Dto.java b/src/main/java/io/github/nikanique/springrestframework/dto/Dto.java
index 391290d..beb6a7c 100644
--- a/src/main/java/io/github/nikanique/springrestframework/dto/Dto.java
+++ b/src/main/java/io/github/nikanique/springrestframework/dto/Dto.java
@@ -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;
@@ -104,4 +105,7 @@ private void validateDateField(java.util.Date value, String fieldName, FieldVali
}
}
+ public void postDeserialization(EntityManager entityManager) {
+ // Default implementation
+ }
}
diff --git a/src/main/java/io/github/nikanique/springrestframework/dto/DtoManager.java b/src/main/java/io/github/nikanique/springrestframework/dto/DtoManager.java
index ea23e20..076e1de 100644
--- a/src/main/java/io/github/nikanique/springrestframework/dto/DtoManager.java
+++ b/src/main/java/io/github/nikanique/springrestframework/dto/DtoManager.java
@@ -22,10 +22,15 @@ public static Map 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
}
@@ -35,7 +40,8 @@ public static Map 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));
});
diff --git a/src/main/java/io/github/nikanique/springrestframework/dto/FieldMetadata.java b/src/main/java/io/github/nikanique/springrestframework/dto/FieldMetadata.java
index ad192da..c6b2691 100644
--- a/src/main/java/io/github/nikanique/springrestframework/dto/FieldMetadata.java
+++ b/src/main/java/io/github/nikanique/springrestframework/dto/FieldMetadata.java
@@ -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;
@@ -27,5 +28,6 @@ public FieldMetadata(Field field, Class> fieldType, FieldValidation validation
this.writeOnly = writeOnly;
this.referencedModel = referencedModel;
this.getterMethodHandle = getterMethodHandle;
+ this.setterMethodHandle = setterMethodHandle;
}
}
diff --git a/src/main/java/io/github/nikanique/springrestframework/orm/EntityBuilder.java b/src/main/java/io/github/nikanique/springrestframework/orm/EntityBuilder.java
index f08c4f0..76c4a8f 100644
--- a/src/main/java/io/github/nikanique/springrestframework/orm/EntityBuilder.java
+++ b/src/main/java/io/github/nikanique/springrestframework/orm/EntityBuilder.java
@@ -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 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;
}
diff --git a/src/main/java/io/github/nikanique/springrestframework/serializer/Serializer.java b/src/main/java/io/github/nikanique/springrestframework/serializer/Serializer.java
index 5171d9a..e1ab247 100644
--- a/src/main/java/io/github/nikanique/springrestframework/serializer/Serializer.java
+++ b/src/main/java/io/github/nikanique/springrestframework/serializer/Serializer.java
@@ -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;
@@ -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 fieldNames = new HashSet<>();
if (partial) {
fieldNames = getPresentFields(requestBody);
@@ -51,17 +59,76 @@ public Object deserialize(String requestBody, Class> dtoClass, Boolean raiseVa
}
- public Object deserialize(String requestBody, Class> dtoClass, Boolean raiseValidationError, Set fields) throws IOException {
- Object dto = objectMapper.readValue(requestBody, dtoClass);
+ public Object deserialize(String requestBody, Class> dtoClass, Boolean raiseValidationError, Set 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 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 getPresentFields(String requestBody) throws JsonProcessingException {
JsonNode requestBodyNode = objectMapper.readTree(requestBody);
Set fieldNames = new HashSet<>();
@@ -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) {
diff --git a/src/main/java/io/github/nikanique/springrestframework/web/controllers/BaseGenericController.java b/src/main/java/io/github/nikanique/springrestframework/web/controllers/BaseGenericController.java
index 861127d..3b23bec 100644
--- a/src/main/java/io/github/nikanique/springrestframework/web/controllers/BaseGenericController.java
+++ b/src/main/java/io/github/nikanique/springrestframework/web/controllers/BaseGenericController.java
@@ -32,14 +32,13 @@ public abstract class BaseGenericController> endpointsRequiredAuthorities;
@Getter
protected Serializer serializer;
protected ApplicationContext context;
- private Map> endpointsRequiredAuthorities;
@Autowired
- public BaseGenericController(@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") ModelRepository repository) {
+ public BaseGenericController(ModelRepository repository) {
this.repository = repository;
this.endpointsRequiredAuthorities = new HashMap<>();
this.configRequiredAuthorities(this.endpointsRequiredAuthorities);
@@ -68,7 +67,7 @@ public void setApplicationContext(ApplicationContext applicationContext) throws
* }
*
*
- * @return
+ * @return The class type of the DTO
*/
protected abstract Class> getDTO();
diff --git a/src/main/java/io/github/nikanique/springrestframework/web/controllers/CreateController.java b/src/main/java/io/github/nikanique/springrestframework/web/controllers/CreateController.java
index d3a01c3..acac063 100644
--- a/src/main/java/io/github/nikanique/springrestframework/web/controllers/CreateController.java
+++ b/src/main/java/io/github/nikanique/springrestframework/web/controllers/CreateController.java
@@ -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.
*
@@ -39,7 +37,7 @@ default SerializerConfig configCreateSerializerFields() {
}
- default ResponseEntity create(BaseGenericController controller, HttpServletRequest request) throws IOException {
+ default ResponseEntity 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());
diff --git a/src/main/java/io/github/nikanique/springrestframework/web/controllers/GenericCommandController.java b/src/main/java/io/github/nikanique/springrestframework/web/controllers/GenericCommandController.java
index 7efbda9..b68196d 100644
--- a/src/main/java/io/github/nikanique/springrestframework/web/controllers/GenericCommandController.java
+++ b/src/main/java/io/github/nikanique/springrestframework/web/controllers/GenericCommandController.java
@@ -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 & JpaSpecificationExecutor>
extends BaseGenericController
@@ -81,7 +79,7 @@ protected Filter configLookupFilter() {
}
@PostMapping("/")
- public ResponseEntity post(HttpServletRequest request) throws IOException {
+ public ResponseEntity post(HttpServletRequest request) throws Throwable {
this.authorizeRequest(request);
return this.create(this, request);
}
diff --git a/src/main/java/io/github/nikanique/springrestframework/web/controllers/GenericCreateController.java b/src/main/java/io/github/nikanique/springrestframework/web/controllers/GenericCreateController.java
index 79dfd86..11423bb 100644
--- a/src/main/java/io/github/nikanique/springrestframework/web/controllers/GenericCreateController.java
+++ b/src/main/java/io/github/nikanique/springrestframework/web/controllers/GenericCreateController.java
@@ -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.
@@ -73,7 +71,7 @@ public Class> getCreateResponseBodyDTO() {
}
@PostMapping("/")
- public ResponseEntity post(HttpServletRequest request) throws IOException {
+ public ResponseEntity post(HttpServletRequest request) throws Throwable {
this.authorizeRequest(request);
return this.create(this, request);
}