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