diff --git a/pom.xml b/pom.xml index 0ddb51f..6dae032 100644 --- a/pom.xml +++ b/pom.xml @@ -13,10 +13,17 @@ spring-boot-db-admin 0.1.2 spring-boot-db-admin - Srping Boot DB Admin Dashboard + Srping Boot Database Admin is an auto-generated CRUD admin panel for Spring Boot apps 17 + + + GPL-v3.0 + http://www.gnu.org/licenses/gpl-3.0.txt + + + release @@ -38,8 +45,8 @@ ALWAYS https://s01.oss.sonatype.org/service/local - false - false + true + true target/staging-deploy diff --git a/src/main/java/tech/ailef/dbadmin/external/DbAdmin.java b/src/main/java/tech/ailef/dbadmin/external/DbAdmin.java index bbb888e..b3eae7a 100644 --- a/src/main/java/tech/ailef/dbadmin/external/DbAdmin.java +++ b/src/main/java/tech/ailef/dbadmin/external/DbAdmin.java @@ -53,6 +53,12 @@ public class DbAdmin { private String modelsPackage; + /** + * Builds the DbAdmin instance by scanning the `@Entity` beans and loading + * the schemas. + * @param entityManager the entity manager + * @param properties the configuration properties + */ public DbAdmin(@Autowired EntityManager entityManager, @Autowired DbAdminProperties properties) { this.modelsPackage = properties.getModelsPackage(); this.entityManager = entityManager; @@ -71,7 +77,7 @@ public class DbAdmin { /** * Returns all the loaded schemas (i.e. entity classes) - * @return + * @return the list of loaded schemas from the `@Entity` classes */ public List getSchemas() { return Collections.unmodifiableList(schemas); @@ -80,7 +86,7 @@ public class DbAdmin { /** * Finds a schema by its full class name * @param className qualified class name - * @return + * @return the schema with this class name * @throws DbAdminException if corresponding schema not found */ public DbObjectSchema findSchemaByClassName(String className) { @@ -92,7 +98,7 @@ public class DbAdmin { /** * Finds a schema by its table name * @param tableName the table name on the database - * @return + * @return the schema with this table name * @throws DbAdminException if corresponding schema not found */ public DbObjectSchema findSchemaByTableName(String tableName) { @@ -102,9 +108,9 @@ public class DbAdmin { } /** - * Finds a schema by its class - * @param klass - * @return + * Finds a schema by its class object + * @param the `@Entity` class you want to find the schema for + * @return the schema for the `@Entity` class * @throws DbAdminException if corresponding schema not found */ public DbObjectSchema findSchemaByClass(Class klass) { @@ -118,7 +124,7 @@ public class DbAdmin { * * If any field is not mappable, the method will throw an exception. * @param bd - * @return + * @return a schema derived from the `@Entity` class */ private DbObjectSchema processBeanDefinition(BeanDefinition bd) { String fullClassName = bd.getBeanClassName(); diff --git a/src/main/java/tech/ailef/dbadmin/external/DbAdminAutoConfiguration.java b/src/main/java/tech/ailef/dbadmin/external/DbAdminAutoConfiguration.java index 7f11371..8899a20 100644 --- a/src/main/java/tech/ailef/dbadmin/external/DbAdminAutoConfiguration.java +++ b/src/main/java/tech/ailef/dbadmin/external/DbAdminAutoConfiguration.java @@ -21,6 +21,10 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; import tech.ailef.dbadmin.internal.InternalDbAdminConfiguration; +/** + * The configuration class that adds and configures the "internal" data source. + * + */ @ConditionalOnProperty(name = "dbadmin.enabled", matchIfMissing = true) @ComponentScan @EnableConfigurationProperties(DbAdminProperties.class) @@ -56,7 +60,7 @@ public class DbAdminAutoConfiguration { LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean(); factoryBean.setDataSource(internalDataSource()); factoryBean.setPersistenceUnitName("internal"); - factoryBean.setPackagesToScan("tech.ailef.dbadmin.internal.model"); // , "tech.ailef.dbadmin.repository"); + factoryBean.setPackagesToScan("tech.ailef.dbadmin.internal.model"); factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter()); Properties properties = new Properties(); properties.setProperty("hibernate.dialect", "org.hibernate.dialect.H2Dialect"); diff --git a/src/main/java/tech/ailef/dbadmin/external/DbAdminProperties.java b/src/main/java/tech/ailef/dbadmin/external/DbAdminProperties.java index c2e3544..5dd458b 100644 --- a/src/main/java/tech/ailef/dbadmin/external/DbAdminProperties.java +++ b/src/main/java/tech/ailef/dbadmin/external/DbAdminProperties.java @@ -26,6 +26,9 @@ public class DbAdminProperties { */ private String modelsPackage; + /** + * Set to true when running the tests to configure the "internal" data source as in memory + */ private boolean testMode = false; public boolean isEnabled() { diff --git a/src/main/java/tech/ailef/dbadmin/external/annotations/DisplayFormat.java b/src/main/java/tech/ailef/dbadmin/external/annotations/DisplayFormat.java index 7e7641e..7e02b04 100644 --- a/src/main/java/tech/ailef/dbadmin/external/annotations/DisplayFormat.java +++ b/src/main/java/tech/ailef/dbadmin/external/annotations/DisplayFormat.java @@ -13,5 +13,9 @@ import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface DisplayFormat { + /** + * The format to apply to the field's value + * @return + */ public String format() default ""; } \ No newline at end of file diff --git a/src/main/java/tech/ailef/dbadmin/external/annotations/Filterable.java b/src/main/java/tech/ailef/dbadmin/external/annotations/Filterable.java index cbcf71e..4d90bf0 100644 --- a/src/main/java/tech/ailef/dbadmin/external/annotations/Filterable.java +++ b/src/main/java/tech/ailef/dbadmin/external/annotations/Filterable.java @@ -16,5 +16,9 @@ import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface Filterable { + /** + * The type of filter (DEFAULT or CATEGORICAL) + * @return + */ public FilterableType type() default FilterableType.DEFAULT; } \ No newline at end of file diff --git a/src/main/java/tech/ailef/dbadmin/external/annotations/FilterableType.java b/src/main/java/tech/ailef/dbadmin/external/annotations/FilterableType.java index 79cf09b..e4a91b1 100644 --- a/src/main/java/tech/ailef/dbadmin/external/annotations/FilterableType.java +++ b/src/main/java/tech/ailef/dbadmin/external/annotations/FilterableType.java @@ -1,5 +1,23 @@ package tech.ailef.dbadmin.external.annotations; +/** + * Type of filters that can be used in the faceted search. + * + */ public enum FilterableType { - DEFAULT, CATEGORICAL; + /** + * The default filter provides a list of standard operators + * customized to the field type (e.g. greater than/less than/equals for numbers, + * after/before/equals for dates, contains/equals for strings, etc...), with, + * if applicable, an autocomplete form if the field references a foreign key. + */ + DEFAULT, + /** + * The categorical filter provides the full list of possible values + * for the field, rendered as a list of clickable items (that will + * filter for equality). This provides a better UX if the field can take + * a limited number of values and it's more convenient to have them all + * on screen rather than typing them. + */ + CATEGORICAL; } diff --git a/src/main/java/tech/ailef/dbadmin/external/controller/DownloadController.java b/src/main/java/tech/ailef/dbadmin/external/controller/DownloadController.java index 36c8863..f46edce 100644 --- a/src/main/java/tech/ailef/dbadmin/external/controller/DownloadController.java +++ b/src/main/java/tech/ailef/dbadmin/external/controller/DownloadController.java @@ -37,6 +37,13 @@ public class DownloadController { private DbAdmin dbAdmin; + /** + * Serve a binary field as an image + * @param className + * @param fieldName + * @param id + * @return + */ @GetMapping(value="/{className}/{fieldName}/{id}/image", produces = MediaType.IMAGE_JPEG_VALUE) @ResponseBody public ResponseEntity serveImage(@PathVariable String className, @@ -58,6 +65,16 @@ public class DownloadController { } + /** + * Serve a binary field as a file. This tries to detect the file type using Tika + * in order to serve the file with a plausible extension, since we don't have + * any meta-data about what was originally uploaded and it is not feasible to + * store it (it could be modified on another end and we wouldn't be aware of it). + * @param className + * @param fieldName + * @param id + * @return + */ @GetMapping("/{className}/{fieldName}/{id}") @ResponseBody public ResponseEntity serveFile(@PathVariable String className, diff --git a/src/main/java/tech/ailef/dbadmin/external/controller/GlobalController.java b/src/main/java/tech/ailef/dbadmin/external/controller/GlobalController.java index 887eea6..8ba6162 100644 --- a/src/main/java/tech/ailef/dbadmin/external/controller/GlobalController.java +++ b/src/main/java/tech/ailef/dbadmin/external/controller/GlobalController.java @@ -43,11 +43,21 @@ public class GlobalController { return props.getBaseUrl(); } + /** + * The full request URL, not including the query string + * @param request + * @return + */ @ModelAttribute("requestUrl") public String getRequestUrl(HttpServletRequest request) { return request.getRequestURI(); } + /** + * The UserConfiguration object used to retrieve values specified + * in the settings table. + * @return + */ @ModelAttribute("userConf") public UserConfiguration getUserConf() { return userConf; diff --git a/src/main/java/tech/ailef/dbadmin/external/controller/rest/AutocompleteController.java b/src/main/java/tech/ailef/dbadmin/external/controller/rest/AutocompleteController.java index 7f42dca..9817fa4 100644 --- a/src/main/java/tech/ailef/dbadmin/external/controller/rest/AutocompleteController.java +++ b/src/main/java/tech/ailef/dbadmin/external/controller/rest/AutocompleteController.java @@ -28,6 +28,12 @@ public class AutocompleteController { @Autowired private DbAdminRepository repository; + /** + * Returns a list of entities from a given table that match an input query. + * @param className + * @param query + * @return + */ @GetMapping("/{className}") public ResponseEntity autocomplete(@PathVariable String className, @RequestParam String query) { DbObjectSchema schema = dbAdmin.findSchemaByClassName(className); 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 702d725..90b32ef 100644 --- a/src/main/java/tech/ailef/dbadmin/external/dbmapping/CustomJpaRepository.java +++ b/src/main/java/tech/ailef/dbadmin/external/dbmapping/CustomJpaRepository.java @@ -80,6 +80,51 @@ public class CustomJpaRepository extends SimpleJpaRepository { .setFirstResult((page - 1) * pageSize).getResultList(); } + + @SuppressWarnings("unchecked") + public int update(DbObjectSchema schema, Map params, Map files) { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + + CriteriaUpdate update = cb.createCriteriaUpdate(schema.getJavaClass()); + + Root root = update.from(schema.getJavaClass()); + + for (DbField field : schema.getSortedFields()) { + if (field.isPrimaryKey()) continue; + + boolean keepValue = params.getOrDefault("__keep_" + field.getName(), "off").equals("on"); + if (keepValue) continue; + + String stringValue = params.get(field.getName()); + Object value = null; + if (stringValue != null && stringValue.isBlank()) stringValue = null; + if (stringValue != null) { + value = field.getType().parseValue(stringValue); + } else { + try { + MultipartFile file = files.get(field.getName()); + if (file != null) { + if (file.isEmpty()) value = null; + else value = file.getBytes(); + } + } catch (IOException e) { + throw new DbAdminException(e); + } + } + + 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(root.get(pkName), params.get(schema.getPrimaryKey().getName()))); + + Query query = entityManager.createQuery(update); + return query.executeUpdate(); + } + @SuppressWarnings("unchecked") private List buildPredicates(String q, Set queryFilters, CriteriaBuilder cb, Path root) { @@ -155,48 +200,4 @@ public class CustomJpaRepository extends SimpleJpaRepository { } return finalPredicates; } - - @SuppressWarnings("unchecked") - public int update(DbObjectSchema schema, Map params, Map files) { - CriteriaBuilder cb = entityManager.getCriteriaBuilder(); - - CriteriaUpdate update = cb.createCriteriaUpdate(schema.getJavaClass()); - - Root root = update.from(schema.getJavaClass()); - - for (DbField field : schema.getSortedFields()) { - if (field.isPrimaryKey()) continue; - - boolean keepValue = params.getOrDefault("__keep_" + field.getName(), "off").equals("on"); - if (keepValue) continue; - - String stringValue = params.get(field.getName()); - Object value = null; - if (stringValue != null && stringValue.isBlank()) stringValue = null; - if (stringValue != null) { - value = field.getType().parseValue(stringValue); - } else { - try { - MultipartFile file = files.get(field.getName()); - if (file != null) { - if (file.isEmpty()) value = null; - else value = file.getBytes(); - } - } catch (IOException e) { - throw new DbAdminException(e); - } - } - - 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(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/external/dbmapping/DbField.java b/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbField.java index 6e5d4c5..2161958 100644 --- a/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbField.java +++ b/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbField.java @@ -13,13 +13,25 @@ import tech.ailef.dbadmin.external.annotations.Filterable; import tech.ailef.dbadmin.external.annotations.FilterableType; public class DbField { + /** + * The inferred name of this field on the database + */ protected String dbName; + /** + * The name of this field in the Java code (instance variable) + */ protected String javaName; + /** + * The type of this field + */ protected DbFieldType type; @JsonIgnore + /** + * The primitive Field object from the Class + */ protected Field field; /** @@ -29,12 +41,25 @@ public class DbField { @JsonIgnore private Class connectedType; + /** + * Whether this field is a primary key + */ private boolean primaryKey; + /** + * Whether this field is nullable + */ private boolean nullable; + /** + * The optional format to apply to this field, if the `@DisplayFormat` + * annotation has been applied. + */ private String format; + /** + * The schema this field belongs to + */ @JsonIgnore private DbObjectSchema schema; 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 8cf1634..3f4eaaf 100644 --- a/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbFieldType.java +++ b/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbFieldType.java @@ -14,6 +14,9 @@ import jakarta.persistence.OneToOne; import tech.ailef.dbadmin.external.dto.CompareOperator; import tech.ailef.dbadmin.external.exceptions.DbAdminException; +/** + * The list of supported field types + */ public enum DbFieldType { INTEGER { @Override @@ -27,7 +30,6 @@ public enum DbFieldType { } @Override - public Class getJavaClass() { return Integer.class; } diff --git a/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbFieldValue.java b/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbFieldValue.java index 6575818..bea6da3 100644 --- a/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbFieldValue.java +++ b/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbFieldValue.java @@ -4,6 +4,10 @@ import java.util.Objects; import com.fasterxml.jackson.annotation.JsonIgnore; +/** + * Wrapper for the value of a field + * + */ public class DbFieldValue { private Object value; 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 0da11bc..42021b9 100644 --- a/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbObject.java +++ b/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbObject.java @@ -15,9 +15,19 @@ import tech.ailef.dbadmin.external.annotations.DisplayName; import tech.ailef.dbadmin.external.exceptions.DbAdminException; import tech.ailef.dbadmin.external.misc.Utils; +/** + * Wrapper for all objects retrieved from the database. + * + */ public class DbObject { + /** + * The instance of the object, i.e. an instance of the `@Entity` class + */ private Object instance; + /** + * The schema this object belongs to + */ private DbObjectSchema schema; public DbObject(Object instance, DbObjectSchema schema) { 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 6b389bd..2ed5929 100644 --- a/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbObjectSchema.java +++ b/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbObjectSchema.java @@ -22,6 +22,11 @@ import tech.ailef.dbadmin.external.annotations.ComputedColumn; import tech.ailef.dbadmin.external.exceptions.DbAdminException; import tech.ailef.dbadmin.external.misc.Utils; +/** + * A class that represents a table/`@Entity` as reconstructed from the + * JPA annotations found on its fields. + * + */ public class DbObjectSchema { /** * All the fields in this table. The fields include all the diff --git a/src/main/java/tech/ailef/dbadmin/external/dto/AutocompleteSearchResult.java b/src/main/java/tech/ailef/dbadmin/external/dto/AutocompleteSearchResult.java index a714bd0..7701972 100644 --- a/src/main/java/tech/ailef/dbadmin/external/dto/AutocompleteSearchResult.java +++ b/src/main/java/tech/ailef/dbadmin/external/dto/AutocompleteSearchResult.java @@ -2,6 +2,11 @@ package tech.ailef.dbadmin.external.dto; import tech.ailef.dbadmin.external.dbmapping.DbObject; +/** + * An object to hold autocomplete results returned from the + * respective AutocompleteController + * + */ public class AutocompleteSearchResult { private Object id; diff --git a/src/main/java/tech/ailef/dbadmin/external/dto/CompareOperator.java b/src/main/java/tech/ailef/dbadmin/external/dto/CompareOperator.java index fbeb373..bc75990 100644 --- a/src/main/java/tech/ailef/dbadmin/external/dto/CompareOperator.java +++ b/src/main/java/tech/ailef/dbadmin/external/dto/CompareOperator.java @@ -1,5 +1,9 @@ package tech.ailef.dbadmin.external.dto; +/** + * A list of operators that are used in faceted search. + * + */ public enum CompareOperator { GT { @Override diff --git a/src/main/java/tech/ailef/dbadmin/external/dto/LogsSearchRequest.java b/src/main/java/tech/ailef/dbadmin/external/dto/LogsSearchRequest.java index e766731..3d66a3a 100644 --- a/src/main/java/tech/ailef/dbadmin/external/dto/LogsSearchRequest.java +++ b/src/main/java/tech/ailef/dbadmin/external/dto/LogsSearchRequest.java @@ -3,19 +3,45 @@ package tech.ailef.dbadmin.external.dto; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; +/** + * A client request for the Action logs page where + * several filtering parameters are present + * + */ public class LogsSearchRequest { + /** + * The table name to filter on + */ private String table; + /** + * The action type to filter on (EDIT, CREATE, DELETE, ANY) + */ private String actionType; + /** + * The item id to filter on. + */ private String itemId; + /** + * The requested page + */ private int page; + /** + * The requested page size + */ private int pageSize; + /** + * The requested sort key + */ private String sortKey; + /** + * The requested sort order + */ private String sortOrder; public String getTable() { @@ -80,6 +106,10 @@ public class LogsSearchRequest { + page + ", pageSize=" + pageSize + ", sortKey=" + sortKey + ", sortOrder=" + sortOrder + "]"; } + /** + * Build a Spring PageRequest object from the parameters in this request + * @return a Spring PageRequest object + */ public PageRequest toPageRequest() { int actualPage = page - 1 < 0 ? 0 : page - 1; int actualPageSize = pageSize <= 0 ? 50 : pageSize; diff --git a/src/main/java/tech/ailef/dbadmin/external/dto/PaginatedResult.java b/src/main/java/tech/ailef/dbadmin/external/dto/PaginatedResult.java index 2b22fda..d389fab 100644 --- a/src/main/java/tech/ailef/dbadmin/external/dto/PaginatedResult.java +++ b/src/main/java/tech/ailef/dbadmin/external/dto/PaginatedResult.java @@ -2,9 +2,19 @@ package tech.ailef.dbadmin.external.dto; import java.util.List; +/** + * A wrapper class that holds info about the current pagination and one page + * of returned result. + */ public class PaginatedResult { + /** + * The pagination settings used to produce this output + */ private PaginationInfo pagination; + /** + * The list of results in the current page + */ private List results; public PaginatedResult(PaginationInfo pagination, List page) { diff --git a/src/main/java/tech/ailef/dbadmin/external/dto/QueryFilter.java b/src/main/java/tech/ailef/dbadmin/external/dto/QueryFilter.java index 9449516..1f252c6 100644 --- a/src/main/java/tech/ailef/dbadmin/external/dto/QueryFilter.java +++ b/src/main/java/tech/ailef/dbadmin/external/dto/QueryFilter.java @@ -29,6 +29,10 @@ public class QueryFilter { return value; } + /** + * Provides a readable version of this query filter, customized + * based on field type and/or operator. + */ @Override public String toString() { if (value != null && !value.toString().isBlank()) {