JPA Validation WIP

This commit is contained in:
Francesco 2023-10-11 10:53:40 +02:00
parent 4b21437c30
commit 2fb76d445f
9 changed files with 111 additions and 43 deletions

View File

@ -33,6 +33,7 @@ import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.jdbc.UncategorizedSQLException; import org.springframework.jdbc.UncategorizedSQLException;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.transaction.TransactionSystemException;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
@ -476,6 +477,20 @@ public class DefaultDbAdminController {
attr.addFlashAttribute("error", "See below for details"); attr.addFlashAttribute("error", "See below for details");
attr.addFlashAttribute("validationErrors", new ValidationErrorsContainer(e)); attr.addFlashAttribute("validationErrors", new ValidationErrorsContainer(e));
attr.addFlashAttribute("params", params); attr.addFlashAttribute("params", params);
} catch (DbAdminException e) {
attr.addFlashAttribute("errorTitle", "Error");
attr.addFlashAttribute("error", e.getMessage());
attr.addFlashAttribute("params", params);
} catch (TransactionSystemException e) {
if (e.getRootCause() instanceof ConstraintViolationException) {
ConstraintViolationException ee = (ConstraintViolationException)e.getRootCause();
attr.addFlashAttribute("errorTitle", "Validation error");
attr.addFlashAttribute("error", "See below for details");
attr.addFlashAttribute("validationErrors", new ValidationErrorsContainer(ee));
attr.addFlashAttribute("params", params);
} else {
throw new RuntimeException(e);
}
} }

View File

@ -89,7 +89,7 @@ public class CustomJpaRepository extends SimpleJpaRepository {
.where( .where(
cb.or( cb.or(
cb.and(finalPredicates.toArray(new Predicate[finalPredicates.size()])), // query search on String fields cb.and(finalPredicates.toArray(new Predicate[finalPredicates.size()])), // query search on String fields
cb.equal(root.get(schema.getPrimaryKey().getName()), q) cb.equal(root.get(schema.getPrimaryKey().getName()).as(String.class), q)
) )
); );

View File

@ -19,9 +19,7 @@
package tech.ailef.dbadmin.external.dbmapping; package tech.ailef.dbadmin.external.dbmapping;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
@ -34,7 +32,6 @@ import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository; import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Component; 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;
@ -216,43 +213,47 @@ public class DbAdminRepository {
@Transactional("transactionManager") @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); DbObject obj = schema.buildObject(values, files);
Object save = save(schema, obj);
Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); return new DbObject(save, schema).getPrimaryKeyValue();
Set<ConstraintViolation<Object>> violations = validator.validate(obj.getUnderlyingInstance()); // return save;
// System.out.println(obj);
if (violations.size() > 0) { // Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
throw new ConstraintViolationException(violations); // validator.va
} // Set<ConstraintViolation<Object>> violations = validator.validate(obj.getUnderlyingInstance());
//
SimpleJdbcInsert insert = new SimpleJdbcInsert(jdbcTemplate).withTableName(schema.getTableName()); // if (violations.size() > 0) {
// throw new ConstraintViolationException(violations);
Map<String, Object> allValues = new HashMap<>(); // }
allValues.putAll(values); //
// SimpleJdbcInsert insert = new SimpleJdbcInsert(jdbcTemplate).withTableName(schema.getTableName());
values.keySet().forEach(fieldName -> { //
if (values.get(fieldName).isBlank()) { // Map<String, Object> allValues = new HashMap<>();
allValues.put(fieldName, null); // allValues.putAll(values);
} //
}); // values.keySet().forEach(fieldName -> {
// if (values.get(fieldName).isBlank()) {
files.keySet().forEach(f -> { // allValues.put(fieldName, null);
try { // }
// The file parameter gets sent even if empty, so it's needed // });
// to check if the file has actual content, to avoid storing an empty file //
if (files.get(f).getSize() > 0) // files.keySet().forEach(f -> {
allValues.put(f, files.get(f).getBytes()); // try {
} catch (IOException e) { // // The file parameter gets sent even if empty, so it's needed
throw new DbAdminException(e); // // to check if the file has actual content, to avoid storing an empty file
} // if (files.get(f).getSize() > 0)
}); // allValues.put(f, files.get(f).getBytes());
// } catch (IOException e) {
if (primaryKey == null) { // throw new DbAdminException(e);
insert = insert.usingGeneratedKeyColumns(schema.getPrimaryKey().getName()); // }
return insert.executeAndReturnKey(allValues); // });
} else { //
insert.execute(allValues); // if (primaryKey == null) {
return primaryKey; // insert = insert.usingGeneratedKeyColumns(schema.getPrimaryKey().getName());
} // return insert.executeAndReturnKey(allValues);
// } else {
// insert.execute(allValues);
// return primaryKey;
// }
} }

View File

@ -27,6 +27,7 @@ import java.util.stream.Collectors;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.ManyToMany; import jakarta.persistence.ManyToMany;
import jakarta.persistence.ManyToOne; import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany; import jakarta.persistence.OneToMany;
@ -208,6 +209,12 @@ public class DbField {
return getPrimitiveField().getAnnotation(ReadOnly.class) != null; return getPrimitiveField().getAnnotation(ReadOnly.class) != null;
} }
public boolean isToOne() {
return (getPrimitiveField().getAnnotation(OneToOne.class) != null &&
getPrimitiveField().getAnnotation(OneToOne.class).mappedBy().isBlank())
|| getPrimitiveField().getAnnotation(ManyToOne.class) != null;
}
/** /**
* Returns if this field is settable with a raw value, i.e. * Returns if this field is settable with a raw value, i.e.
* a field that is not a relationship to another entity; * a field that is not a relationship to another entity;
@ -220,6 +227,10 @@ public class DbField {
&& getPrimitiveField().getAnnotation(ManyToMany.class) == null; && getPrimitiveField().getAnnotation(ManyToMany.class) == null;
} }
public boolean isGeneratedValue() {
return getPrimitiveField().getAnnotation(GeneratedValue.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

@ -32,7 +32,6 @@ import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne; import jakarta.persistence.OneToOne;
import tech.ailef.dbadmin.external.annotations.DisplayName; import tech.ailef.dbadmin.external.annotations.DisplayName;
import tech.ailef.dbadmin.external.exceptions.DbAdminException; import tech.ailef.dbadmin.external.exceptions.DbAdminException;
import tech.ailef.dbadmin.external.misc.Utils;
/** /**
* Wrapper for all objects retrieved from the database. * Wrapper for all objects retrieved from the database.
@ -183,6 +182,29 @@ public class DbObject {
} }
} }
@SuppressWarnings("unchecked")
public void setRelationship(String fieldName, Object primaryKeyValue) {
DbField field = schema.getFieldByName(fieldName);
DbObjectSchema linkedSchema = field.getConnectedSchema();
Optional<?> obj = linkedSchema.getJpaRepository().findById(primaryKeyValue);
if (!obj.isPresent()) {
throw new DbAdminException("Invalid value " + primaryKeyValue + " for " + fieldName
+ ": item does not exist.");
}
Method setter = findSetter(field.getJavaName());
if (setter == null) {
throw new DbAdminException("Unable to find setter method for " + fieldName + " in " + schema.getClassName());
}
try {
setter.invoke(instance, obj.get());
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
}
}
public void set(String fieldName, Object value) { public void set(String fieldName, Object value) {
Method setter = findSetter(fieldName); Method setter = findSetter(fieldName);
@ -229,4 +251,11 @@ public class DbObject {
return null; return null;
} }
@Override
public String toString() {
return "DbObject [instance=" + instance + ", schema=" + schema + "]";
}
} }

View File

@ -384,6 +384,10 @@ public class DbObjectSchema {
if (parsedFieldValue != null && getFieldByName(param).isSettable()) { if (parsedFieldValue != null && getFieldByName(param).isSettable()) {
setter.invoke(instance, parsedFieldValue); setter.invoke(instance, parsedFieldValue);
} }
if (parsedFieldValue != null && getFieldByName(param).isToOne()) {
dbObject.setRelationship(param, parsedFieldValue);
}
} }
for (String fileParam : files.keySet()) { for (String fileParam : files.keySet()) {

View File

@ -29,4 +29,11 @@ public class ValidationErrorsContainer {
public boolean isEmpty() { public boolean isEmpty() {
return errors.isEmpty(); return errors.isEmpty();
} }
@Override
public String toString() {
return "ValidationErrorsContainer [errors=" + errors + "]";
}
} }

View File

@ -31,6 +31,7 @@
<form class="form" enctype="multipart/form-data" method="post" th:action="|/${dbadmin_baseUrl}/model/${className}/create|"> <form class="form" enctype="multipart/form-data" method="post" th:action="|/${dbadmin_baseUrl}/model/${className}/create|">
<input type="hidden" name="__dbadmin_create" th:value="${create}"> <input type="hidden" name="__dbadmin_create" th:value="${create}">
<div th:each="field : ${schema.getSortedFields(false)}" class="mt-2" <div th:each="field : ${schema.getSortedFields(false)}" class="mt-2"
th:if="${!field.isGeneratedValue() || !create}"
th:classAppend="|${validationErrors != null && validationErrors.hasErrors(field.getJavaName()) ? 'invalid' : ''}|"> th:classAppend="|${validationErrors != null && validationErrors.hasErrors(field.getJavaName()) ? 'invalid' : ''}|">
<label th:for="|__id_${field.getName()}|" class="mb-1 fw-bold"> <label th:for="|__id_${field.getName()}|" class="mb-1 fw-bold">
<span th:if="${!field.isNullable() && !field.isPrimaryKey()}"> <span th:if="${!field.isNullable() && !field.isPrimaryKey()}">

View File

@ -68,7 +68,7 @@
<td> <td>
<i class="bi bi-cpu"></i> <i class="bi bi-cpu"></i>
</td> </td>
<td th:text="${colName}"> <td class="fw-bold" th:text="${colName}">
</td> </td>
<td th:text="${object.compute(colName)}"> <td th:text="${object.compute(colName)}">
</td> </td>