WIP JPA Validation (TODO: show error messages on fields)

This commit is contained in:
Francesco 2023-10-10 17:53:45 +02:00
parent 8eab71ff55
commit 8b88475d8a
7 changed files with 136 additions and 22 deletions

View File

@ -27,6 +27,7 @@ import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.hibernate.id.IdentifierGenerationException;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.HttpStatus; 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.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.ConstraintViolationException;
import tech.ailef.dbadmin.external.DbAdmin; import tech.ailef.dbadmin.external.DbAdmin;
import tech.ailef.dbadmin.external.DbAdminProperties; import tech.ailef.dbadmin.external.DbAdminProperties;
import tech.ailef.dbadmin.external.dbmapping.DbAdminRepository; import tech.ailef.dbadmin.external.dbmapping.DbAdminRepository;
@ -417,7 +419,7 @@ public class DefaultDbAdminController {
String c = params.get("__dbadmin_create"); String c = params.get("__dbadmin_create");
if (c == null) { if (c == null) {
throw new ResponseStatusException( 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(); pkValue = newPrimaryKey.toString();
attr.addFlashAttribute("message", "Item created successfully."); attr.addFlashAttribute("message", "Item created successfully.");
saveAction(new UserAction(schema.getTableName(), pkValue, "CREATE", schema.getClassName())); 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("errorTitle", "Unable to INSERT row");
attr.addFlashAttribute("error", e.getMessage()); attr.addFlashAttribute("error", e.getMessage());
attr.addFlashAttribute("params", params); attr.addFlashAttribute("params", params);
} catch (UncategorizedSQLException e) { } catch (ConstraintViolationException e) {
attr.addFlashAttribute("errorTitle", "Unable to INSERT row"); attr.addFlashAttribute("errorTitle", "Unable to INSERT row (no changes applied)");
attr.addFlashAttribute("error", e.getMessage()); attr.addFlashAttribute("error", e.toString());
attr.addFlashAttribute("params", params); attr.addFlashAttribute("params", params);
} }
@ -467,13 +469,13 @@ public class DefaultDbAdminController {
repository.attachManyToMany(schema, pkValue, multiValuedParams); repository.attachManyToMany(schema, pkValue, multiValuedParams);
attr.addFlashAttribute("message", "Item saved successfully."); attr.addFlashAttribute("message", "Item saved successfully.");
saveAction(new UserAction(schema.getTableName(), pkValue, "EDIT", schema.getClassName())); 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("errorTitle", "Unable to UPDATE row (no changes applied)");
attr.addFlashAttribute("error", e.getMessage()); attr.addFlashAttribute("error", e.getMessage());
attr.addFlashAttribute("params", params); attr.addFlashAttribute("params", params);
} catch (IllegalArgumentException e) { } catch (ConstraintViolationException e) {
attr.addFlashAttribute("errorTitle", "Unable to UPDATE row (no changes applied)"); attr.addFlashAttribute("errorTitle", "Unable to INSERT row (no changes applied)");
attr.addFlashAttribute("error", e.getMessage()); attr.addFlashAttribute("error", e.toString());
attr.addFlashAttribute("params", params); attr.addFlashAttribute("params", params);
} }
} }
@ -483,10 +485,14 @@ public class DefaultDbAdminController {
repository.attachManyToMany(schema, newPrimaryKey, multiValuedParams); repository.attachManyToMany(schema, newPrimaryKey, multiValuedParams);
attr.addFlashAttribute("message", "Item created successfully"); attr.addFlashAttribute("message", "Item created successfully");
saveAction(new UserAction(schema.getTableName(), pkValue, "CREATE", schema.getClassName())); 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("errorTitle", "Unable to INSERT row (no changes applied)");
attr.addFlashAttribute("error", e.getMessage()); attr.addFlashAttribute("error", e.getMessage());
attr.addFlashAttribute("params", params); 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);
} }
} }
} }

View File

@ -108,6 +108,7 @@ public class CustomJpaRepository extends SimpleJpaRepository {
Root root = update.from(schema.getJavaClass()); Root root = update.from(schema.getJavaClass());
boolean hasUpdate = false;
for (DbField field : schema.getSortedFields()) { for (DbField field : schema.getSortedFields()) {
if (field.isPrimaryKey()) continue; if (field.isPrimaryKey()) continue;
if (field.isReadOnly()) continue; if (field.isReadOnly()) continue;
@ -136,8 +137,11 @@ public class CustomJpaRepository extends SimpleJpaRepository {
value = field.getConnectedSchema().getJpaRepository().findById(value).get(); value = field.getConnectedSchema().getJpaRepository().findById(value).get();
update.set(root.get(field.getJavaName()), value); update.set(root.get(field.getJavaName()), value);
hasUpdate = true;
} }
if (!hasUpdate) return 0;
String pkName = schema.getPrimaryKey().getJavaName(); String pkName = schema.getPrimaryKey().getJavaName();
update.where(cb.equal(root.get(pkName), params.get(schema.getPrimaryKey().getName()))); update.where(cb.equal(root.get(pkName), params.get(schema.getPrimaryKey().getName())));

View File

@ -39,6 +39,10 @@ import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile; 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.FacetedSearchRequest;
import tech.ailef.dbadmin.external.dto.PaginatedResult; import tech.ailef.dbadmin.external.dto.PaginatedResult;
import tech.ailef.dbadmin.external.dto.PaginationInfo; import tech.ailef.dbadmin.external.dto.PaginationInfo;
@ -151,13 +155,22 @@ public class DbAdminRepository {
*/ */
@Transactional("transactionManager") @Transactional("transactionManager")
public void update(DbObjectSchema schema, Map<String, String> params, Map<String, MultipartFile> files) { public void update(DbObjectSchema schema, Map<String, String> params, Map<String, MultipartFile> files) {
DbObject obj = schema.buildObject(params, files);
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Set<ConstraintViolation<Object>> violations = validator.validate(obj.getUnderlyingInstance());
if (violations.size() > 0) {
throw new ConstraintViolationException(violations);
}
schema.getJpaRepository().update(schema, params, files); schema.getJpaRepository().update(schema, params, files);
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Transactional("transactionManager") @Transactional("transactionManager")
private void save(DbObjectSchema schema, DbObject o) { private Object save(DbObjectSchema schema, DbObject o) {
schema.getJpaRepository().save(o.getUnderlyingInstance()); return schema.getJpaRepository().save(o.getUnderlyingInstance());
} }
@Transactional("transactionManager") @Transactional("transactionManager")
@ -185,7 +198,7 @@ public class DbAdminRepository {
} }
dbObject.set( dbObject.set(
fieldName, field.getJavaName(),
traverseMany.stream().map(o -> o.getUnderlyingInstance()).collect(Collectors.toList()) traverseMany.stream().map(o -> o.getUnderlyingInstance()).collect(Collectors.toList())
); );
} }
@ -200,7 +213,17 @@ public class DbAdminRepository {
* @param values * @param values
* @param primaryKey * @param primaryKey
*/ */
@Transactional("transactionManager")
public Object create(DbObjectSchema schema, Map<String, String> values, Map<String, MultipartFile> files, String primaryKey) { public Object create(DbObjectSchema schema, Map<String, String> values, Map<String, MultipartFile> files, String primaryKey) {
DbObject obj = schema.buildObject(values, files);
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Set<ConstraintViolation<Object>> violations = validator.validate(obj.getUnderlyingInstance());
if (violations.size() > 0) {
throw new ConstraintViolationException(violations);
}
SimpleJdbcInsert insert = new SimpleJdbcInsert(jdbcTemplate).withTableName(schema.getTableName()); SimpleJdbcInsert insert = new SimpleJdbcInsert(jdbcTemplate).withTableName(schema.getTableName());
Map<String, Object> allValues = new HashMap<>(); Map<String, Object> allValues = new HashMap<>();

View File

@ -27,6 +27,10 @@ import java.util.stream.Collectors;
import com.fasterxml.jackson.annotation.JsonIgnore; 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.DisplayImage;
import tech.ailef.dbadmin.external.annotations.Filterable; import tech.ailef.dbadmin.external.annotations.Filterable;
import tech.ailef.dbadmin.external.annotations.FilterableType; import tech.ailef.dbadmin.external.annotations.FilterableType;
@ -204,6 +208,18 @@ public class DbField {
return getPrimitiveField().getAnnotation(ReadOnly.class) != null; 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<DbFieldValue> getAllValues() { public Set<DbFieldValue> getAllValues() {
List<?> findAll = schema.getJpaRepository().findAll(); List<?> findAll = schema.getJpaRepository().findAll();
return findAll.stream() return findAll.stream()

View File

@ -51,6 +51,7 @@ public enum DbFieldType {
@Override @Override
public Object parseValue(Object value) { public Object parseValue(Object value) {
if (value == null || value.toString().isBlank()) return null;
return Short.parseShort(value.toString()); return Short.parseShort(value.toString());
} }
@ -72,7 +73,7 @@ public enum DbFieldType {
@Override @Override
public Object parseValue(Object value) { public Object parseValue(Object value) {
if (value == null) return null; if (value == null || value.toString().isBlank()) return null;
return new BigInteger(value.toString()); return new BigInteger(value.toString());
} }
@ -94,6 +95,7 @@ public enum DbFieldType {
@Override @Override
public Object parseValue(Object value) { public Object parseValue(Object value) {
if (value == null || value.toString().isBlank()) return null;
return Integer.parseInt(value.toString()); return Integer.parseInt(value.toString());
} }
@ -115,6 +117,7 @@ public enum DbFieldType {
@Override @Override
public Object parseValue(Object value) { public Object parseValue(Object value) {
if (value == null || value.toString().isBlank()) return null;
return Double.parseDouble(value.toString()); return Double.parseDouble(value.toString());
} }
@ -136,6 +139,7 @@ public enum DbFieldType {
@Override @Override
public Object parseValue(Object value) { public Object parseValue(Object value) {
if (value == null || value.toString().isBlank()) return null;
return Long.parseLong(value.toString()); return Long.parseLong(value.toString());
} }
@ -157,6 +161,7 @@ public enum DbFieldType {
@Override @Override
public Object parseValue(Object value) { public Object parseValue(Object value) {
if (value == null || value.toString().isBlank()) return null;
return Float.parseFloat(value.toString()); return Float.parseFloat(value.toString());
} }
@ -178,7 +183,7 @@ public enum DbFieldType {
@Override @Override
public Object parseValue(Object value) { public Object parseValue(Object value) {
if (value == null) return null; if (value == null || value.toString().isBlank()) return null;
return OffsetDateTime.parse(value.toString()); return OffsetDateTime.parse(value.toString());
} }
@ -201,7 +206,7 @@ public enum DbFieldType {
@Override @Override
public Object parseValue(Object value) { 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); SimpleDateFormat format = new SimpleDateFormat("dd-MM-yyyy", Locale.ENGLISH);
try { try {
return format.parse(value.toString()); return format.parse(value.toString());
@ -228,7 +233,7 @@ public enum DbFieldType {
@Override @Override
public Object parseValue(Object value) { public Object parseValue(Object value) {
if (value == null) return null; if (value == null || value.toString().isBlank()) return null;
return LocalDate.parse(value.toString()); return LocalDate.parse(value.toString());
} }
@ -272,7 +277,8 @@ public enum DbFieldType {
@Override @Override
public Object parseValue(Object value) { public Object parseValue(Object value) {
return value; if (value == null || value.toString().isBlank()) return null;
return value.toString();
} }
@Override @Override
@ -293,7 +299,8 @@ public enum DbFieldType {
@Override @Override
public Object parseValue(Object value) { public Object parseValue(Object value) {
return value; if (value == null || value.toString().isBlank()) return null;
return value.toString();
} }
@Override @Override
@ -315,6 +322,7 @@ public enum DbFieldType {
@Override @Override
public Object parseValue(Object value) { public Object parseValue(Object value) {
if (value == null || value.toString().isBlank()) return null;
return Boolean.parseBoolean(value.toString()); return Boolean.parseBoolean(value.toString());
} }
@ -336,6 +344,7 @@ public enum DbFieldType {
@Override @Override
public Object parseValue(Object value) { public Object parseValue(Object value) {
if (value == null || value.toString().isBlank()) return null;
return new BigDecimal(value.toString()); return new BigDecimal(value.toString());
} }
@ -357,6 +366,8 @@ public enum DbFieldType {
@Override @Override
public Object parseValue(Object value) { public Object parseValue(Object value) {
if (value == null || value.toString().isBlank()) return null;
if (value.toString().isBlank()) return null;
return value.toString().charAt(0); return value.toString().charAt(0);
} }
@ -378,6 +389,7 @@ public enum DbFieldType {
@Override @Override
public Object parseValue(Object value) { public Object parseValue(Object value) {
if (value == null || value.toString().isBlank()) return null;
return value.toString().getBytes()[0]; return value.toString().getBytes()[0];
} }
@ -399,6 +411,7 @@ public enum DbFieldType {
@Override @Override
public Object parseValue(Object value) { public Object parseValue(Object value) {
if (value == null || value.toString().isBlank()) return null;
try { try {
return ((MultipartFile)value).getBytes(); return ((MultipartFile)value).getBytes();
} catch (IOException e) { } catch (IOException e) {

View File

@ -198,8 +198,7 @@ public class DbObject {
} }
private Method findSetter(String fieldName) { protected Method findSetter(String fieldName) {
fieldName = Utils.snakeToCamel(fieldName);
String capitalize = Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1); String capitalize = Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);
Method[] methods = instance.getClass().getDeclaredMethods(); Method[] methods = instance.getClass().getDeclaredMethods();
@ -211,7 +210,7 @@ public class DbObject {
return null; return null;
} }
private Method findGetter(String fieldName) { protected Method findGetter(String fieldName) {
String capitalize = Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1); String capitalize = Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);
Method[] methods = instance.getClass().getDeclaredMethods(); Method[] methods = instance.getClass().getDeclaredMethods();

View File

@ -19,6 +19,7 @@
package tech.ailef.dbadmin.external.dbmapping; package tech.ailef.dbadmin.external.dbmapping;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -30,6 +31,8 @@ import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.springframework.web.multipart.MultipartFile;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.ManyToMany; import jakarta.persistence.ManyToMany;
@ -359,6 +362,56 @@ public class DbObjectSchema {
return r.stream().map(o -> new DbObject(o, this)).toList(); return r.stream().map(o -> new DbObject(o, this)).toList();
} }
public DbObject buildObject(Map<String, String> params, Map<String, MultipartFile> 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 @Override
public String toString() { public String toString() {
return "DbObjectSchema [fields=" + fields + ", className=" + entityClass.getName() + "]"; return "DbObjectSchema [fields=" + fields + ", className=" + entityClass.getName() + "]";