From 49112fe60b2d53c7b39d79d4b83d55cb41995557 Mon Sep 17 00:00:00 2001 From: Francesco Date: Thu, 21 Sep 2023 10:55:07 +0200 Subject: [PATCH] WIP --- README.md | 3 +- .../dbadmin/ApplicationContextUtils.java | 3 + src/main/java/tech/ailef/dbadmin/DbAdmin.java | 94 +++++++++++++------ .../dbadmin/annotations/ComputedColumn.java | 6 ++ .../annotations/DbAdminAppConfiguration.java | 5 + .../annotations/DbAdminConfiguration.java | 4 + .../dbadmin/annotations/DisplayFormat.java | 5 + .../dbadmin/annotations/DisplayImage.java | 5 + .../dbadmin/annotations/DisplayName.java | 6 ++ .../ailef/dbadmin/annotations/Filterable.java | 8 ++ .../controller/DefaultDbAdminController.java | 70 +++++++++----- .../controller/DownloadController.java | 3 + .../dbmapping/AdvancedJpaRepository.java | 16 ++-- .../dbadmin/dbmapping/DbAdminRepository.java | 9 -- .../ailef/dbadmin/dbmapping/DbObject.java | 4 +- .../dbadmin/dbmapping/DbObjectSchema.java | 8 +- 16 files changed, 177 insertions(+), 72 deletions(-) diff --git a/README.md b/README.md index c8651e0..38a0b13 100644 --- a/README.md +++ b/README.md @@ -110,8 +110,7 @@ This annotation can be placed on binary fields to declare they are storing an im ## Changelog -0.0.3 - @DisplayImage - +0.0.3 - @DisplayImage; Selenium tests; Fixed/greatly improved edit page; 0.0.2 - Faceted search with `@Filterable` annotation diff --git a/src/main/java/tech/ailef/dbadmin/ApplicationContextUtils.java b/src/main/java/tech/ailef/dbadmin/ApplicationContextUtils.java index bb1794f..0529431 100644 --- a/src/main/java/tech/ailef/dbadmin/ApplicationContextUtils.java +++ b/src/main/java/tech/ailef/dbadmin/ApplicationContextUtils.java @@ -4,6 +4,9 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component; +/** + * Utility class the get the ApplicationContext + */ @Component public class ApplicationContextUtils implements ApplicationContextAware { diff --git a/src/main/java/tech/ailef/dbadmin/DbAdmin.java b/src/main/java/tech/ailef/dbadmin/DbAdmin.java index c0c3427..c96fff3 100644 --- a/src/main/java/tech/ailef/dbadmin/DbAdmin.java +++ b/src/main/java/tech/ailef/dbadmin/DbAdmin.java @@ -35,6 +35,15 @@ import tech.ailef.dbadmin.dbmapping.DbObjectSchema; import tech.ailef.dbadmin.exceptions.DbAdminException; import tech.ailef.dbadmin.misc.Utils; +/** + * The main DbAdmin class responsible for the initialization phase. This class scans + * the user provided package containing the `@Entity` definitions and tries to map each + * entity to a DbObjectSchema instance. + * + * This process involves determining the correct type for each class field and its + * configuration at the database level. An exception will be thrown if it's not possible + * to determine the field type. + */ @Component public class DbAdmin { @PersistenceContext @@ -57,10 +66,6 @@ public class DbAdmin { this.modelsPackage = applicationClass.getModelsPackage(); this.entityManager = entityManager; - init(); - } - - public void init() { ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); provider.addIncludeFilter(new AnnotationTypeFilter(Entity.class)); @@ -69,7 +74,46 @@ public class DbAdmin { schemas.add(processBeanDefinition(bd)); } } + + /** + * Returns all the loaded schemas (i.e. entity classes) + * @return + */ + public List getSchemas() { + return Collections.unmodifiableList(schemas); + } + /** + * Finds a schema by its full class name + * @param className qualified class name + * @return + * @throws DbAdminException if corresponding schema not found + */ + public DbObjectSchema findSchemaByClassName(String className) { + return schemas.stream().filter(s -> s.getClassName().equals(className)).findFirst().orElseThrow(() -> { + return new DbAdminException("Schema " + className + " not found."); + }); + } + + /** + * Finds a schema by its class + * @param klass + * @return + * @throws DbAdminException if corresponding schema not found + */ + public DbObjectSchema findSchemaByClass(Class klass) { + return findSchemaByClassName(klass.getName()); + } + + + /** + * This method processes a BeanDefinition into a DbObjectSchema object, + * where all fields have been correctly mapped to DbField objects. + * + * If any field is not mappable, the method will throw an exception. + * @param bd + * @return + */ private DbObjectSchema processBeanDefinition(BeanDefinition bd) { String fullClassName = bd.getBeanClassName(); @@ -88,9 +132,7 @@ public class DbAdmin { System.out.println(" - Mapping field " + f); DbField field = mapField(f, schema); if (field == null) { -// continue; - // TODO: CHECK THIS EXCEPTION - throw new DbAdminException("IMPOSSIBLE TO MAP FIELD: " + f); + throw new DbAdminException("Impossible to map field: " + f); } field.setSchema(schema); @@ -123,6 +165,11 @@ public class DbAdmin { return fieldName; } + /** + * Determines if a field is nullable from the `@Column` annotation + * @param f + * @return + */ private boolean determineNullable(Field f) { Column[] columnAnnotations = f.getAnnotationsByType(Column.class); @@ -135,6 +182,15 @@ public class DbAdmin { return nullable; } + /** + * Builds a DbField object from a primitive Java field. This process involves + * determining the correct field name on the database, its type and additional + * attributes (e.g. nullable). + * This method returns null if a field cannot be mapped to a supported type. + * @param f primitive Java field to construct a DbField from + * @param schema the schema this field belongs to + * @return + */ private DbField mapField(Field f, DbObjectSchema schema) { OneToMany oneToMany = f.getAnnotation(OneToMany.class); ManyToMany manyToMany = f.getAnnotation(ManyToMany.class); @@ -208,7 +264,7 @@ public class DbAdmin { * Returns the type of a foreign key field, by looking at the type * of the primary key (defined as `@Id`) in the referenced table. * - * @param f + * @param entityClass * @return */ private DbFieldType mapForeignKeyType(Class entityClass) { @@ -231,26 +287,4 @@ public class DbAdmin { throw new DbAdminException(e); } } - - public String getBasePackage() { - return modelsPackage; - } - - public List getSchemas() { - return Collections.unmodifiableList(schemas); - } - - public DbObjectSchema findSchemaByClassName(String className) { - return schemas.stream().filter(s -> s.getClassName().equals(className)).findFirst().orElseThrow(() -> { - return new DbAdminException("Schema " + className + " not found."); - }); - } - - public DbObjectSchema findSchemaByClass(Class klass) { - return findSchemaByClassName(klass.getName()); - } - - public EntityManager getEntityManager() { - return entityManager; - } } diff --git a/src/main/java/tech/ailef/dbadmin/annotations/ComputedColumn.java b/src/main/java/tech/ailef/dbadmin/annotations/ComputedColumn.java index bef12d8..3ddd1d9 100644 --- a/src/main/java/tech/ailef/dbadmin/annotations/ComputedColumn.java +++ b/src/main/java/tech/ailef/dbadmin/annotations/ComputedColumn.java @@ -5,6 +5,12 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * This annotation marks a method as a "virtual" whose value is computed by + * using the method itself rather than retrieving it like a physical column + * from the database. + * + */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface ComputedColumn { diff --git a/src/main/java/tech/ailef/dbadmin/annotations/DbAdminAppConfiguration.java b/src/main/java/tech/ailef/dbadmin/annotations/DbAdminAppConfiguration.java index 8902aa6..ee53fa7 100644 --- a/src/main/java/tech/ailef/dbadmin/annotations/DbAdminAppConfiguration.java +++ b/src/main/java/tech/ailef/dbadmin/annotations/DbAdminAppConfiguration.java @@ -1,5 +1,10 @@ package tech.ailef.dbadmin.annotations; +/** + * An interface that includes all the configuration methods that + * the user has to implement in order to integrate DbAdmin. + * + */ public interface DbAdminAppConfiguration { public String getModelsPackage(); } diff --git a/src/main/java/tech/ailef/dbadmin/annotations/DbAdminConfiguration.java b/src/main/java/tech/ailef/dbadmin/annotations/DbAdminConfiguration.java index e562249..3050ffe 100644 --- a/src/main/java/tech/ailef/dbadmin/annotations/DbAdminConfiguration.java +++ b/src/main/java/tech/ailef/dbadmin/annotations/DbAdminConfiguration.java @@ -5,6 +5,10 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * Marks the class that holds the DbAdmin configuration. + * + */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface DbAdminConfiguration { diff --git a/src/main/java/tech/ailef/dbadmin/annotations/DisplayFormat.java b/src/main/java/tech/ailef/dbadmin/annotations/DisplayFormat.java index 9f24233..4f5637c 100644 --- a/src/main/java/tech/ailef/dbadmin/annotations/DisplayFormat.java +++ b/src/main/java/tech/ailef/dbadmin/annotations/DisplayFormat.java @@ -5,6 +5,11 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * Specifies a format string for a field, which will be automatically applied + * when displaying its value. + * + */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface DisplayFormat { diff --git a/src/main/java/tech/ailef/dbadmin/annotations/DisplayImage.java b/src/main/java/tech/ailef/dbadmin/annotations/DisplayImage.java index 8828301..6daee38 100644 --- a/src/main/java/tech/ailef/dbadmin/annotations/DisplayImage.java +++ b/src/main/java/tech/ailef/dbadmin/annotations/DisplayImage.java @@ -5,6 +5,11 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * Marks a binary field as containing an image, which in turn enables + * its display in the interface. + * + */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface DisplayImage { diff --git a/src/main/java/tech/ailef/dbadmin/annotations/DisplayName.java b/src/main/java/tech/ailef/dbadmin/annotations/DisplayName.java index b4855bb..9a6030f 100644 --- a/src/main/java/tech/ailef/dbadmin/annotations/DisplayName.java +++ b/src/main/java/tech/ailef/dbadmin/annotations/DisplayName.java @@ -5,6 +5,12 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * Marks a method as returning a name that has to be used to display + * this item, in addition to its primary key. Use to give users more + * readable item names. + * + */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface DisplayName { diff --git a/src/main/java/tech/ailef/dbadmin/annotations/Filterable.java b/src/main/java/tech/ailef/dbadmin/annotations/Filterable.java index 5b72c56..1792021 100644 --- a/src/main/java/tech/ailef/dbadmin/annotations/Filterable.java +++ b/src/main/java/tech/ailef/dbadmin/annotations/Filterable.java @@ -5,6 +5,14 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * Marks a field as filterable and places it in the faceted search bar. + * (This bar only appears in the interface if one or more fields are filterable + * in the current schema.) + * Can only be placed on fields that correspond to physical columns on the + * table (e.g. no `@ManyToMany`/`@OneToMany`) and that are not binary (`byte[]`). + * + */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface Filterable { diff --git a/src/main/java/tech/ailef/dbadmin/controller/DefaultDbAdminController.java b/src/main/java/tech/ailef/dbadmin/controller/DefaultDbAdminController.java index 944e039..f42e14b 100644 --- a/src/main/java/tech/ailef/dbadmin/controller/DefaultDbAdminController.java +++ b/src/main/java/tech/ailef/dbadmin/controller/DefaultDbAdminController.java @@ -11,6 +11,7 @@ import java.util.stream.Collectors; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpStatus; +import org.springframework.jdbc.UncategorizedSQLException; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.util.LinkedMultiValueMap; @@ -36,29 +37,11 @@ import tech.ailef.dbadmin.dto.QueryFilter; import tech.ailef.dbadmin.exceptions.InvalidPageException; import tech.ailef.dbadmin.misc.Utils; +/** + * The main DbAdmin controller that register most of the routes of the web interface. + */ @Controller @RequestMapping("/dbadmin") -/** - * FOR 0.0.3: - * @DisplayImage DONE TODO: write docs in README - * Fixed/improved edit page for binary fields (files) DONE - * - * TODO - * - double data source for internal database and settings - * - role based authorization (PRO) - * - Pagination in one to many results? - * - AI console (PRO) - * - Action logs - * - Boolean icons - * - Boolean in create/edit is checkbox - * - Documentation - * - SQL console (PRO) - * - JPA Validation (PRO) - * - Logging - * - Selenium tests - * - Logs in web ui - * - Tests: AutocompleteController, REST API, create/edit - */ public class DefaultDbAdminController { @Autowired private DbAdminRepository repository; @@ -66,6 +49,12 @@ public class DefaultDbAdminController { @Autowired private DbAdmin dbAdmin; + /** + * Home page with list of schemas + * @param model + * @param query + * @return + */ @GetMapping public String index(Model model, @RequestParam(required = false) String query) { List schemas = dbAdmin.getSchemas(); @@ -88,6 +77,24 @@ public class DefaultDbAdminController { return "home"; } + /** + * Lists the items of a schema by applying a variety of filters: + * - query: fuzzy search + * - otherParams: filterable fields + * Includes pagination and sorting options. + * + * @param model + * @param className + * @param page + * @param query + * @param pageSize + * @param sortKey + * @param sortOrder + * @param otherParams + * @param request + * @param response + * @return + */ @GetMapping("/model/{className}") public String list(Model model, @PathVariable String className, @RequestParam(required=false) Integer page, @RequestParam(required=false) String query, @@ -160,6 +167,12 @@ public class DefaultDbAdminController { } } + /** + * Displays information about the schema + * @param model + * @param className + * @return + */ @GetMapping("/model/{className}/schema") public String schema(Model model, @PathVariable String className) { DbObjectSchema schema = dbAdmin.findSchemaByClassName(className); @@ -170,6 +183,13 @@ public class DefaultDbAdminController { return "model/schema"; } + /** + * Shows a single item + * @param model + * @param className + * @param id + * @return + */ @GetMapping("/model/{className}/show/{id}") public String show(Model model, @PathVariable String className, @PathVariable String id) { DbObjectSchema schema = dbAdmin.findSchemaByClassName(className); @@ -333,6 +353,10 @@ public class DefaultDbAdminController { 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()); + attr.addFlashAttribute("params", params); } } else { @@ -352,6 +376,10 @@ public class DefaultDbAdminController { 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()); + attr.addFlashAttribute("params", params); } } } else { diff --git a/src/main/java/tech/ailef/dbadmin/controller/DownloadController.java b/src/main/java/tech/ailef/dbadmin/controller/DownloadController.java index 7b43588..7542cc0 100644 --- a/src/main/java/tech/ailef/dbadmin/controller/DownloadController.java +++ b/src/main/java/tech/ailef/dbadmin/controller/DownloadController.java @@ -24,6 +24,9 @@ import tech.ailef.dbadmin.dbmapping.DbObject; import tech.ailef.dbadmin.dbmapping.DbObjectSchema; import tech.ailef.dbadmin.exceptions.DbAdminException; +/** + * Controller to serve file or images (`@DisplayImage`) + */ @Controller @RequestMapping("/dbadmin/download") public class DownloadController { diff --git a/src/main/java/tech/ailef/dbadmin/dbmapping/AdvancedJpaRepository.java b/src/main/java/tech/ailef/dbadmin/dbmapping/AdvancedJpaRepository.java index 05c6a7a..9904b2d 100644 --- a/src/main/java/tech/ailef/dbadmin/dbmapping/AdvancedJpaRepository.java +++ b/src/main/java/tech/ailef/dbadmin/dbmapping/AdvancedJpaRepository.java @@ -159,16 +159,13 @@ public class AdvancedJpaRepository extends SimpleJpaRepository { CriteriaUpdate update = cb.createCriteriaUpdate(schema.getJavaClass()); - Root employee = update.from(schema.getJavaClass()); + Root root = update.from(schema.getJavaClass()); for (DbField field : schema.getSortedFields()) { if (field.isPrimaryKey()) continue; - boolean keepValue = params.getOrDefault("__keep_" + field.getJavaName(), "off").equals("on"); - - if (keepValue) { - continue; - } + boolean keepValue = params.getOrDefault("__keep_" + field.getName(), "off").equals("on"); + if (keepValue) continue; String stringValue = params.get(field.getName()); Object value = null; @@ -187,11 +184,14 @@ public class AdvancedJpaRepository extends SimpleJpaRepository { } } - update.set(employee.get(field.getJavaName()), value); + if (field.getConnectedSchema() != null) + value = field.getConnectedSchema().getJpaRepository().findById(value).get(); + + update.set(root.get(field.getJavaName()), value); } String pkName = schema.getPrimaryKey().getJavaName(); - update.where(cb.equal(employee.get(pkName), params.get(schema.getPrimaryKey().getName()))); + update.where(cb.equal(root.get(pkName), params.get(schema.getPrimaryKey().getName()))); Query query = entityManager.createQuery(update); return query.executeUpdate(); diff --git a/src/main/java/tech/ailef/dbadmin/dbmapping/DbAdminRepository.java b/src/main/java/tech/ailef/dbadmin/dbmapping/DbAdminRepository.java index 9a70c99..a227ef2 100644 --- a/src/main/java/tech/ailef/dbadmin/dbmapping/DbAdminRepository.java +++ b/src/main/java/tech/ailef/dbadmin/dbmapping/DbAdminRepository.java @@ -131,16 +131,7 @@ public class DbAdminRepository { */ @Transactional public void update(DbObjectSchema schema, Map params, Map files) { -// Object[] updateArray = schema.getUpdateArray(params, files); -// -// String updateFields = -// schema.getSortedFields().stream().map(f -> "`" + f.getName() + "` = ?").collect(Collectors.joining(", ")); -// -// String query = "UPDATE `" + schema.getTableName() + "` SET " + updateFields + " WHERE `" + schema.getPrimaryKey().getName() + "` = ?"; -// jdbcTemplate.update(query, updateArray); - schema.getJpaRepository().update(schema, params, files); - } @SuppressWarnings("unchecked") diff --git a/src/main/java/tech/ailef/dbadmin/dbmapping/DbObject.java b/src/main/java/tech/ailef/dbadmin/dbmapping/DbObject.java index efa3647..f4dc026 100644 --- a/src/main/java/tech/ailef/dbadmin/dbmapping/DbObject.java +++ b/src/main/java/tech/ailef/dbadmin/dbmapping/DbObject.java @@ -125,7 +125,9 @@ public class DbObject { if (displayNameMethod.isPresent()) { try { - return displayNameMethod.get().invoke(instance).toString(); + Object displayName = displayNameMethod.get().invoke(instance); + if (displayName == null) return null; + else return displayName.toString(); } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { throw new DbAdminException(e); } diff --git a/src/main/java/tech/ailef/dbadmin/dbmapping/DbObjectSchema.java b/src/main/java/tech/ailef/dbadmin/dbmapping/DbObjectSchema.java index 9e50aff..0578909 100644 --- a/src/main/java/tech/ailef/dbadmin/dbmapping/DbObjectSchema.java +++ b/src/main/java/tech/ailef/dbadmin/dbmapping/DbObjectSchema.java @@ -14,6 +14,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.ManyToMany; import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; import jakarta.persistence.Table; import tech.ailef.dbadmin.DbAdmin; import tech.ailef.dbadmin.annotations.ComputedColumn; @@ -128,8 +129,13 @@ public class DbObjectSchema { public List getSortedFields() { return getFields().stream() .filter(f -> { - return f.getPrimitiveField().getAnnotation(OneToMany.class) == null + boolean toMany = f.getPrimitiveField().getAnnotation(OneToMany.class) == null && f.getPrimitiveField().getAnnotation(ManyToMany.class) == null; + + OneToOne oneToOne = f.getPrimitiveField().getAnnotation(OneToOne.class); + boolean mappedBy = oneToOne != null && !oneToOne.mappedBy().isBlank(); + + return toMany && !mappedBy; }) .sorted((a, b) -> { if (a.isPrimaryKey() && !b.isPrimaryKey())