From 8b88475d8a694269b50c7746583d2d706e043fe0 Mon Sep 17 00:00:00 2001 From: Francesco Date: Tue, 10 Oct 2023 17:53:45 +0200 Subject: [PATCH] WIP JPA Validation (TODO: show error messages on fields) --- .../controller/DefaultDbAdminController.java | 26 +++++---- .../dbmapping/CustomJpaRepository.java | 4 ++ .../external/dbmapping/DbAdminRepository.java | 29 ++++++++-- .../dbadmin/external/dbmapping/DbField.java | 16 ++++++ .../external/dbmapping/DbFieldType.java | 25 ++++++--- .../dbadmin/external/dbmapping/DbObject.java | 5 +- .../external/dbmapping/DbObjectSchema.java | 53 +++++++++++++++++++ 7 files changed, 136 insertions(+), 22 deletions(-) diff --git a/src/main/java/tech/ailef/dbadmin/external/controller/DefaultDbAdminController.java b/src/main/java/tech/ailef/dbadmin/external/controller/DefaultDbAdminController.java index c75a0d9..f4251be 100644 --- a/src/main/java/tech/ailef/dbadmin/external/controller/DefaultDbAdminController.java +++ b/src/main/java/tech/ailef/dbadmin/external/controller/DefaultDbAdminController.java @@ -27,6 +27,7 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import org.hibernate.id.IdentifierGenerationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpStatus; @@ -46,6 +47,7 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.ConstraintViolationException; import tech.ailef.dbadmin.external.DbAdmin; import tech.ailef.dbadmin.external.DbAdminProperties; import tech.ailef.dbadmin.external.dbmapping.DbAdminRepository; @@ -417,7 +419,7 @@ public class DefaultDbAdminController { String c = params.get("__dbadmin_create"); if (c == null) { throw new ResponseStatusException( - HttpStatus.INTERNAL_SERVER_ERROR, "Missing required param __dbadmin_create" + HttpStatus.BAD_REQUEST, "Missing required param __dbadmin_create" ); } @@ -443,13 +445,13 @@ public class DefaultDbAdminController { pkValue = newPrimaryKey.toString(); attr.addFlashAttribute("message", "Item created successfully."); saveAction(new UserAction(schema.getTableName(), pkValue, "CREATE", schema.getClassName())); - } catch (DataIntegrityViolationException e) { + } catch (DataIntegrityViolationException | UncategorizedSQLException | IdentifierGenerationException e) { attr.addFlashAttribute("errorTitle", "Unable to INSERT row"); attr.addFlashAttribute("error", e.getMessage()); attr.addFlashAttribute("params", params); - } catch (UncategorizedSQLException e) { - attr.addFlashAttribute("errorTitle", "Unable to INSERT row"); - attr.addFlashAttribute("error", e.getMessage()); + } catch (ConstraintViolationException e) { + attr.addFlashAttribute("errorTitle", "Unable to INSERT row (no changes applied)"); + attr.addFlashAttribute("error", e.toString()); attr.addFlashAttribute("params", params); } @@ -467,13 +469,13 @@ public class DefaultDbAdminController { repository.attachManyToMany(schema, pkValue, multiValuedParams); attr.addFlashAttribute("message", "Item saved successfully."); saveAction(new UserAction(schema.getTableName(), pkValue, "EDIT", schema.getClassName())); - } catch (DataIntegrityViolationException e) { + } catch (DataIntegrityViolationException | UncategorizedSQLException | IdentifierGenerationException e) { attr.addFlashAttribute("errorTitle", "Unable to UPDATE row (no changes applied)"); attr.addFlashAttribute("error", e.getMessage()); attr.addFlashAttribute("params", params); - } catch (IllegalArgumentException e) { - attr.addFlashAttribute("errorTitle", "Unable to UPDATE row (no changes applied)"); - attr.addFlashAttribute("error", e.getMessage()); + } catch (ConstraintViolationException e) { + attr.addFlashAttribute("errorTitle", "Unable to INSERT row (no changes applied)"); + attr.addFlashAttribute("error", e.toString()); attr.addFlashAttribute("params", params); } } @@ -483,10 +485,14 @@ public class DefaultDbAdminController { repository.attachManyToMany(schema, newPrimaryKey, multiValuedParams); attr.addFlashAttribute("message", "Item created successfully"); saveAction(new UserAction(schema.getTableName(), pkValue, "CREATE", schema.getClassName())); - } catch (DataIntegrityViolationException e) { + } catch (DataIntegrityViolationException | UncategorizedSQLException | IdentifierGenerationException e) { attr.addFlashAttribute("errorTitle", "Unable to INSERT row (no changes applied)"); attr.addFlashAttribute("error", e.getMessage()); attr.addFlashAttribute("params", params); + } catch (ConstraintViolationException e) { + attr.addFlashAttribute("errorTitle", "Unable to INSERT row (no changes applied)"); + attr.addFlashAttribute("error", e.toString()); + attr.addFlashAttribute("params", params); } } } diff --git a/src/main/java/tech/ailef/dbadmin/external/dbmapping/CustomJpaRepository.java b/src/main/java/tech/ailef/dbadmin/external/dbmapping/CustomJpaRepository.java index ddd0e52..d4eaca9 100644 --- a/src/main/java/tech/ailef/dbadmin/external/dbmapping/CustomJpaRepository.java +++ b/src/main/java/tech/ailef/dbadmin/external/dbmapping/CustomJpaRepository.java @@ -108,6 +108,7 @@ public class CustomJpaRepository extends SimpleJpaRepository { Root root = update.from(schema.getJavaClass()); + boolean hasUpdate = false; for (DbField field : schema.getSortedFields()) { if (field.isPrimaryKey()) continue; if (field.isReadOnly()) continue; @@ -136,8 +137,11 @@ public class CustomJpaRepository extends SimpleJpaRepository { value = field.getConnectedSchema().getJpaRepository().findById(value).get(); update.set(root.get(field.getJavaName()), value); + hasUpdate = true; } + if (!hasUpdate) return 0; + String pkName = schema.getPrimaryKey().getJavaName(); update.where(cb.equal(root.get(pkName), params.get(schema.getPrimaryKey().getName()))); diff --git a/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbAdminRepository.java b/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbAdminRepository.java index 57a2de9..89ad372 100644 --- a/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbAdminRepository.java +++ b/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbAdminRepository.java @@ -39,6 +39,10 @@ import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import jakarta.validation.Validation; +import jakarta.validation.Validator; import tech.ailef.dbadmin.external.dto.FacetedSearchRequest; import tech.ailef.dbadmin.external.dto.PaginatedResult; import tech.ailef.dbadmin.external.dto.PaginationInfo; @@ -151,13 +155,22 @@ public class DbAdminRepository { */ @Transactional("transactionManager") public void update(DbObjectSchema schema, Map params, Map files) { + DbObject obj = schema.buildObject(params, files); + + Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); + Set> violations = validator.validate(obj.getUnderlyingInstance()); + + if (violations.size() > 0) { + throw new ConstraintViolationException(violations); + } + schema.getJpaRepository().update(schema, params, files); } @SuppressWarnings("unchecked") @Transactional("transactionManager") - private void save(DbObjectSchema schema, DbObject o) { - schema.getJpaRepository().save(o.getUnderlyingInstance()); + private Object save(DbObjectSchema schema, DbObject o) { + return schema.getJpaRepository().save(o.getUnderlyingInstance()); } @Transactional("transactionManager") @@ -185,7 +198,7 @@ public class DbAdminRepository { } dbObject.set( - fieldName, + field.getJavaName(), traverseMany.stream().map(o -> o.getUnderlyingInstance()).collect(Collectors.toList()) ); } @@ -200,7 +213,17 @@ public class DbAdminRepository { * @param values * @param primaryKey */ + @Transactional("transactionManager") public Object create(DbObjectSchema schema, Map values, Map files, String primaryKey) { + DbObject obj = schema.buildObject(values, files); + + Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); + Set> violations = validator.validate(obj.getUnderlyingInstance()); + + if (violations.size() > 0) { + throw new ConstraintViolationException(violations); + } + SimpleJdbcInsert insert = new SimpleJdbcInsert(jdbcTemplate).withTableName(schema.getTableName()); Map allValues = new HashMap<>(); diff --git a/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbField.java b/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbField.java index 4699328..0bbb435 100644 --- a/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbField.java +++ b/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbField.java @@ -27,6 +27,10 @@ import java.util.stream.Collectors; import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; import tech.ailef.dbadmin.external.annotations.DisplayImage; import tech.ailef.dbadmin.external.annotations.Filterable; import tech.ailef.dbadmin.external.annotations.FilterableType; @@ -204,6 +208,18 @@ public class DbField { return getPrimitiveField().getAnnotation(ReadOnly.class) != null; } + /** + * Returns if this field is settable with a raw value, i.e. + * a field that is not a relationship to another entity; + * @return + */ + public boolean isSettable() { + return getPrimitiveField().getAnnotation(ManyToOne.class) == null + && getPrimitiveField().getAnnotation(OneToMany.class) == null + && getPrimitiveField().getAnnotation(OneToOne.class) == null + && getPrimitiveField().getAnnotation(ManyToMany.class) == null; + } + public Set getAllValues() { List findAll = schema.getJpaRepository().findAll(); return findAll.stream() diff --git a/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbFieldType.java b/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbFieldType.java index 80741fc..10b3768 100644 --- a/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbFieldType.java +++ b/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbFieldType.java @@ -51,6 +51,7 @@ public enum DbFieldType { @Override public Object parseValue(Object value) { + if (value == null || value.toString().isBlank()) return null; return Short.parseShort(value.toString()); } @@ -72,7 +73,7 @@ public enum DbFieldType { @Override public Object parseValue(Object value) { - if (value == null) return null; + if (value == null || value.toString().isBlank()) return null; return new BigInteger(value.toString()); } @@ -94,6 +95,7 @@ public enum DbFieldType { @Override public Object parseValue(Object value) { + if (value == null || value.toString().isBlank()) return null; return Integer.parseInt(value.toString()); } @@ -115,6 +117,7 @@ public enum DbFieldType { @Override public Object parseValue(Object value) { + if (value == null || value.toString().isBlank()) return null; return Double.parseDouble(value.toString()); } @@ -136,6 +139,7 @@ public enum DbFieldType { @Override public Object parseValue(Object value) { + if (value == null || value.toString().isBlank()) return null; return Long.parseLong(value.toString()); } @@ -157,6 +161,7 @@ public enum DbFieldType { @Override public Object parseValue(Object value) { + if (value == null || value.toString().isBlank()) return null; return Float.parseFloat(value.toString()); } @@ -178,7 +183,7 @@ public enum DbFieldType { @Override public Object parseValue(Object value) { - if (value == null) return null; + if (value == null || value.toString().isBlank()) return null; return OffsetDateTime.parse(value.toString()); } @@ -201,7 +206,7 @@ public enum DbFieldType { @Override public Object parseValue(Object value) { - if (value == null) return null; + if (value == null || value.toString().isBlank()) return null; SimpleDateFormat format = new SimpleDateFormat("dd-MM-yyyy", Locale.ENGLISH); try { return format.parse(value.toString()); @@ -228,7 +233,7 @@ public enum DbFieldType { @Override public Object parseValue(Object value) { - if (value == null) return null; + if (value == null || value.toString().isBlank()) return null; return LocalDate.parse(value.toString()); } @@ -272,7 +277,8 @@ public enum DbFieldType { @Override public Object parseValue(Object value) { - return value; + if (value == null || value.toString().isBlank()) return null; + return value.toString(); } @Override @@ -293,7 +299,8 @@ public enum DbFieldType { @Override public Object parseValue(Object value) { - return value; + if (value == null || value.toString().isBlank()) return null; + return value.toString(); } @Override @@ -315,6 +322,7 @@ public enum DbFieldType { @Override public Object parseValue(Object value) { + if (value == null || value.toString().isBlank()) return null; return Boolean.parseBoolean(value.toString()); } @@ -336,6 +344,7 @@ public enum DbFieldType { @Override public Object parseValue(Object value) { + if (value == null || value.toString().isBlank()) return null; return new BigDecimal(value.toString()); } @@ -357,6 +366,8 @@ public enum DbFieldType { @Override public Object parseValue(Object value) { + if (value == null || value.toString().isBlank()) return null; + if (value.toString().isBlank()) return null; return value.toString().charAt(0); } @@ -378,6 +389,7 @@ public enum DbFieldType { @Override public Object parseValue(Object value) { + if (value == null || value.toString().isBlank()) return null; return value.toString().getBytes()[0]; } @@ -399,6 +411,7 @@ public enum DbFieldType { @Override public Object parseValue(Object value) { + if (value == null || value.toString().isBlank()) return null; try { return ((MultipartFile)value).getBytes(); } catch (IOException e) { diff --git a/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbObject.java b/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbObject.java index d266641..b23dac2 100644 --- a/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbObject.java +++ b/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbObject.java @@ -198,8 +198,7 @@ public class DbObject { } - private Method findSetter(String fieldName) { - fieldName = Utils.snakeToCamel(fieldName); + protected Method findSetter(String fieldName) { String capitalize = Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1); Method[] methods = instance.getClass().getDeclaredMethods(); @@ -211,7 +210,7 @@ public class DbObject { return null; } - private Method findGetter(String fieldName) { + protected Method findGetter(String fieldName) { String capitalize = Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1); Method[] methods = instance.getClass().getDeclaredMethods(); diff --git a/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbObjectSchema.java b/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbObjectSchema.java index de8f51f..512ad35 100644 --- a/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbObjectSchema.java +++ b/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbObjectSchema.java @@ -19,6 +19,7 @@ package tech.ailef.dbadmin.external.dbmapping; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; @@ -30,6 +31,8 @@ import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; +import org.springframework.web.multipart.MultipartFile; + import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.ManyToMany; @@ -359,6 +362,56 @@ public class DbObjectSchema { return r.stream().map(o -> new DbObject(o, this)).toList(); } + public DbObject buildObject(Map params, Map files) { + try { + Object instance = getJavaClass().getConstructor().newInstance(); + DbObject dbObject = new DbObject(instance, this); + + for (String param : params.keySet()) { + // Parameters starting with __ are hidden and not related to the object creation + if (param.startsWith("__")) continue; + + String javaFieldName = getFieldByName(param).getJavaName(); + Method setter = dbObject.findSetter(javaFieldName); + + if (setter == null) { + throw new RuntimeException("Cannot find setter for " + javaFieldName); + } + + Object parsedFieldValue = + getFieldByName(param).getType().parseValue(params.get(param)); + + if (parsedFieldValue != null && getFieldByName(param).isSettable()) { + setter.invoke(instance, parsedFieldValue); + } + } + + for (String fileParam : files.keySet()) { + if (fileParam.startsWith("__")) continue; + + String javaFieldName = getFieldByName(fileParam).getJavaName(); + Method setter = dbObject.findSetter(javaFieldName); + + if (setter == null) { + throw new RuntimeException("Cannot find setter for " + fileParam); + } + + Object parsedFieldValue = + getFieldByName(fileParam).getType().parseValue(params.get(fileParam)); + + if (parsedFieldValue != null && getFieldByName(fileParam).isSettable()) { + setter.invoke(instance, parsedFieldValue); + } + } + + return dbObject; + } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException + | NoSuchMethodException | SecurityException e) { + throw new RuntimeException(e); + } + + } + @Override public String toString() { return "DbObjectSchema [fields=" + fields + ", className=" + entityClass.getName() + "]";