mirror of
https://github.com/dalbodeule/snap-admin.git
synced 2025-12-16 05:12:00 +09:00
0.1.0
This commit is contained in:
295
src/main/java/tech/ailef/dbadmin/external/DbAdmin.java
vendored
Normal file
295
src/main/java/tech/ailef/dbadmin/external/DbAdmin.java
vendored
Normal file
@@ -0,0 +1,295 @@
|
||||
package tech.ailef.dbadmin.external;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
|
||||
import org.springframework.core.type.filter.AnnotationTypeFilter;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToMany;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.OneToMany;
|
||||
import jakarta.persistence.OneToOne;
|
||||
import tech.ailef.dbadmin.external.annotations.DisplayFormat;
|
||||
import tech.ailef.dbadmin.external.dbmapping.AdvancedJpaRepository;
|
||||
import tech.ailef.dbadmin.external.dbmapping.DbField;
|
||||
import tech.ailef.dbadmin.external.dbmapping.DbFieldType;
|
||||
import tech.ailef.dbadmin.external.dbmapping.DbObjectSchema;
|
||||
import tech.ailef.dbadmin.external.exceptions.DbAdminException;
|
||||
import tech.ailef.dbadmin.external.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 {
|
||||
private static final Logger logger = Logger.getLogger(DbAdmin.class.getName());
|
||||
|
||||
// @PersistenceContext
|
||||
private EntityManager entityManager;
|
||||
|
||||
private List<DbObjectSchema> schemas = new ArrayList<>();
|
||||
|
||||
private String modelsPackage;
|
||||
|
||||
public DbAdmin(@Autowired EntityManager entityManager, @Autowired DbAdminProperties properties) {
|
||||
this.modelsPackage = properties.getModelsPackage();
|
||||
this.entityManager = entityManager;
|
||||
|
||||
ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);
|
||||
provider.addIncludeFilter(new AnnotationTypeFilter(Entity.class));
|
||||
|
||||
Set<BeanDefinition> beanDefs = provider.findCandidateComponents(modelsPackage);
|
||||
for (BeanDefinition bd : beanDefs) {
|
||||
schemas.add(processBeanDefinition(bd));
|
||||
}
|
||||
|
||||
logger.info("Spring Boot Database Admin initialized. Loaded " + schemas.size() + " table definitions");
|
||||
logger.info("Spring Boot Database Admin web interface at: http://YOUR_HOST:YOUR_PORT/" + properties.getBaseUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the loaded schemas (i.e. entity classes)
|
||||
* @return
|
||||
*/
|
||||
public List<DbObjectSchema> 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 table name
|
||||
* @param tableName the table name on the database
|
||||
* @return
|
||||
* @throws DbAdminException if corresponding schema not found
|
||||
*/
|
||||
public DbObjectSchema findSchemaByTableName(String tableName) {
|
||||
return schemas.stream().filter(s -> s.getTableName().equals(tableName)).findFirst().orElseThrow(() -> {
|
||||
return new DbAdminException("Schema " + tableName + " 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();
|
||||
|
||||
try {
|
||||
Class<?> klass = Class.forName(fullClassName);
|
||||
DbObjectSchema schema = new DbObjectSchema(klass, this);
|
||||
AdvancedJpaRepository simpleJpaRepository = new AdvancedJpaRepository(schema, entityManager);
|
||||
schema.setJpaRepository(simpleJpaRepository);
|
||||
|
||||
System.out.println("\n\n******************************************************");
|
||||
System.out.println("* Class: " + klass + " - Table: " + schema.getTableName());
|
||||
System.out.println("******************************************************");
|
||||
|
||||
Field[] fields = klass.getDeclaredFields();
|
||||
for (Field f : fields) {
|
||||
System.out.println(" - Mapping field " + f);
|
||||
DbField field = mapField(f, schema);
|
||||
if (field == null) {
|
||||
throw new DbAdminException("Impossible to map field: " + f);
|
||||
}
|
||||
field.setSchema(schema);
|
||||
|
||||
schema.addField(field);
|
||||
}
|
||||
|
||||
return schema;
|
||||
} catch (ClassNotFoundException |
|
||||
IllegalArgumentException | SecurityException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the name for the given field, by transforming it to snake_case
|
||||
* and checking if the `@Column` annotation is present.
|
||||
* @param f
|
||||
* @return
|
||||
*/
|
||||
private String determineFieldName(Field f) {
|
||||
Column[] columnAnnotations = f.getAnnotationsByType(Column.class);
|
||||
String fieldName = Utils.camelToSnake(f.getName());
|
||||
|
||||
if (columnAnnotations.length != 0) {
|
||||
Column col = columnAnnotations[0];
|
||||
if (col.name() != null && !col.name().isBlank())
|
||||
fieldName = col.name();
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
boolean nullable = true;
|
||||
if (columnAnnotations.length != 0) {
|
||||
Column col = columnAnnotations[0];
|
||||
nullable = col.nullable();
|
||||
}
|
||||
|
||||
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);
|
||||
ManyToOne manyToOne = f.getAnnotation(ManyToOne.class);
|
||||
OneToOne oneToOne = f.getAnnotation(OneToOne.class);
|
||||
|
||||
String fieldName = determineFieldName(f);
|
||||
|
||||
// This will contain the type of the entity linked by the
|
||||
// foreign key, if any
|
||||
Class<?> connectedType = null;
|
||||
|
||||
// Try to assign default field type
|
||||
DbFieldType fieldType = null;
|
||||
try {
|
||||
fieldType = DbFieldType.fromClass(f.getType());
|
||||
} catch (DbAdminException e) {
|
||||
// If failure, we try to map a relationship on this field
|
||||
}
|
||||
|
||||
if (manyToOne != null || oneToOne != null) {
|
||||
fieldName = mapRelationshipJoinColumn(f);
|
||||
fieldType = mapForeignKeyType(f.getType());
|
||||
connectedType = f.getType();
|
||||
}
|
||||
|
||||
if (manyToMany != null || oneToMany != null) {
|
||||
ParameterizedType stringListType = (ParameterizedType) f.getGenericType();
|
||||
Class<?> targetEntityClass = (Class<?>) stringListType.getActualTypeArguments()[0];
|
||||
fieldType = mapForeignKeyType(targetEntityClass);
|
||||
connectedType = targetEntityClass;
|
||||
}
|
||||
|
||||
if (fieldType == null) {
|
||||
throw new DbAdminException("Unable to determine fieldType for " + f.getType());
|
||||
}
|
||||
|
||||
DisplayFormat displayFormat = f.getAnnotation(DisplayFormat.class);
|
||||
|
||||
DbField field = new DbField(f.getName(), fieldName, f, fieldType, schema, displayFormat != null ? displayFormat.format() : null);
|
||||
field.setConnectedType(connectedType);
|
||||
|
||||
Id[] idAnnotations = f.getAnnotationsByType(Id.class);
|
||||
field.setPrimaryKey(idAnnotations.length != 0);
|
||||
|
||||
field.setNullable(determineNullable(f));
|
||||
|
||||
if (field.isPrimaryKey())
|
||||
field.setNullable(false);
|
||||
|
||||
return field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the join column name for the relationship defined on
|
||||
* the input Field object.
|
||||
* @param f
|
||||
* @return
|
||||
*/
|
||||
private String mapRelationshipJoinColumn(Field f) {
|
||||
String joinColumnName = Utils.camelToSnake(f.getName()) + "_id";
|
||||
JoinColumn[] joinColumn = f.getAnnotationsByType(JoinColumn.class);
|
||||
if (joinColumn.length != 0) {
|
||||
joinColumnName = joinColumn[0].name();
|
||||
}
|
||||
return joinColumnName;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 entityClass
|
||||
* @return
|
||||
*/
|
||||
private DbFieldType mapForeignKeyType(Class<?> entityClass) {
|
||||
try {
|
||||
Object linkedEntity = entityClass.getConstructor().newInstance();
|
||||
Class<?> linkType = null;
|
||||
|
||||
for (Field ef : linkedEntity.getClass().getDeclaredFields()) {
|
||||
if (ef.getAnnotationsByType(Id.class).length != 0) {
|
||||
linkType = ef.getType();
|
||||
}
|
||||
}
|
||||
|
||||
if (linkType == null)
|
||||
throw new DbAdminException("Unable to find @Id field in Entity class " + entityClass);
|
||||
|
||||
return DbFieldType.fromClass(linkType);
|
||||
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException
|
||||
| InvocationTargetException | NoSuchMethodException | SecurityException e) {
|
||||
throw new DbAdminException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
76
src/main/java/tech/ailef/dbadmin/external/DbAdminAutoConfiguration.java
vendored
Normal file
76
src/main/java/tech/ailef/dbadmin/external/DbAdminAutoConfiguration.java
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
package tech.ailef.dbadmin.external;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.jdbc.DataSourceBuilder;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||
import org.springframework.orm.jpa.JpaTransactionManager;
|
||||
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
|
||||
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
|
||||
import tech.ailef.dbadmin.internal.InternalDbAdminConfiguration;
|
||||
|
||||
@ConditionalOnProperty(name = "dbadmin.enabled", matchIfMissing = true)
|
||||
@ComponentScan
|
||||
@EnableConfigurationProperties(DbAdminProperties.class)
|
||||
@Configuration
|
||||
@EnableJpaRepositories(
|
||||
entityManagerFactoryRef = "internalEntityManagerFactory",
|
||||
transactionManagerRef = "internalTransactionManager",
|
||||
basePackages = { "tech.ailef.dbadmin.internal.repository" }
|
||||
)
|
||||
@EnableTransactionManagement
|
||||
@Import(InternalDbAdminConfiguration.class)
|
||||
public class DbAdminAutoConfiguration {
|
||||
@Autowired
|
||||
private DbAdminProperties props;
|
||||
|
||||
@Bean
|
||||
public DataSource internalDataSource() {
|
||||
DataSourceBuilder<?> dataSourceBuilder = DataSourceBuilder.create();
|
||||
dataSourceBuilder.driverClassName("org.h2.Driver");
|
||||
if (props.isTestMode()) {
|
||||
dataSourceBuilder.url("jdbc:h2:mem:test");
|
||||
} else {
|
||||
dataSourceBuilder.url("jdbc:h2:file:./dbadmin_internal");
|
||||
}
|
||||
|
||||
dataSourceBuilder.username("sa");
|
||||
dataSourceBuilder.password("password");
|
||||
return dataSourceBuilder.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public LocalContainerEntityManagerFactoryBean internalEntityManagerFactory() {
|
||||
LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();
|
||||
factoryBean.setDataSource(internalDataSource());
|
||||
factoryBean.setPersistenceUnitName("internal");
|
||||
factoryBean.setPackagesToScan("tech.ailef.dbadmin.internal.model"); // , "tech.ailef.dbadmin.repository");
|
||||
factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
|
||||
Properties properties = new Properties();
|
||||
properties.setProperty("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
|
||||
properties.setProperty("hibernate.hbm2ddl.auto", "update");
|
||||
factoryBean.setJpaProperties(properties);
|
||||
factoryBean.afterPropertiesSet();
|
||||
return factoryBean;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PlatformTransactionManager internalTransactionManager() {
|
||||
JpaTransactionManager transactionManager = new JpaTransactionManager();
|
||||
transactionManager.setEntityManagerFactory(internalEntityManagerFactory().getObject());
|
||||
return transactionManager;
|
||||
}
|
||||
|
||||
}
|
||||
73
src/main/java/tech/ailef/dbadmin/external/DbAdminProperties.java
vendored
Normal file
73
src/main/java/tech/ailef/dbadmin/external/DbAdminProperties.java
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
package tech.ailef.dbadmin.external;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* The 'dbadmin.*' properties that can be set in the properties file
|
||||
* to configure the behaviour of Spring Boot Admin Panel.
|
||||
*/
|
||||
@ConfigurationProperties("dbadmin")
|
||||
public class DbAdminProperties {
|
||||
/**
|
||||
* Whether Spring Boot Database Admin is enabled.
|
||||
*/
|
||||
public boolean enabled = true;
|
||||
|
||||
/**
|
||||
* The prefix that is prepended to all routes registered by Spring Boot Database Admin.
|
||||
*/
|
||||
private String baseUrl;
|
||||
|
||||
/**
|
||||
* The path of the package that contains your JPA `@Entity` classes to be scanned.
|
||||
*/
|
||||
private String modelsPackage;
|
||||
|
||||
private boolean testMode = false;
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public String getBaseUrl() {
|
||||
return baseUrl;
|
||||
}
|
||||
|
||||
public void setBaseUrl(String baseUrl) {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
public String getModelsPackage() {
|
||||
return modelsPackage;
|
||||
}
|
||||
|
||||
public void setModelsPackage(String modelsPackage) {
|
||||
this.modelsPackage = modelsPackage;
|
||||
}
|
||||
|
||||
public boolean isTestMode() {
|
||||
return testMode;
|
||||
}
|
||||
|
||||
public void setTestMode(boolean testMode) {
|
||||
this.testMode = testMode;
|
||||
}
|
||||
|
||||
public Map<String, String> toMap() {
|
||||
Map<String, String> conf = new HashMap<>();
|
||||
conf.put("enabled", enabled + "");
|
||||
conf.put("baseUrl", baseUrl);
|
||||
conf.put("modelsPackage", modelsPackage);
|
||||
conf.put("testMode", testMode + "");
|
||||
return conf;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
18
src/main/java/tech/ailef/dbadmin/external/annotations/ComputedColumn.java
vendored
Normal file
18
src/main/java/tech/ailef/dbadmin/external/annotations/ComputedColumn.java
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
package tech.ailef.dbadmin.external.annotations;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
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 {
|
||||
public String name() default "";
|
||||
}
|
||||
17
src/main/java/tech/ailef/dbadmin/external/annotations/DisplayFormat.java
vendored
Normal file
17
src/main/java/tech/ailef/dbadmin/external/annotations/DisplayFormat.java
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
package tech.ailef.dbadmin.external.annotations;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
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 {
|
||||
public String format() default "";
|
||||
}
|
||||
16
src/main/java/tech/ailef/dbadmin/external/annotations/DisplayImage.java
vendored
Normal file
16
src/main/java/tech/ailef/dbadmin/external/annotations/DisplayImage.java
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
package tech.ailef.dbadmin.external.annotations;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
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 {
|
||||
}
|
||||
17
src/main/java/tech/ailef/dbadmin/external/annotations/DisplayName.java
vendored
Normal file
17
src/main/java/tech/ailef/dbadmin/external/annotations/DisplayName.java
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
package tech.ailef.dbadmin.external.annotations;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
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 {
|
||||
}
|
||||
19
src/main/java/tech/ailef/dbadmin/external/annotations/Filterable.java
vendored
Normal file
19
src/main/java/tech/ailef/dbadmin/external/annotations/Filterable.java
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
package tech.ailef.dbadmin.external.annotations;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
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 {
|
||||
}
|
||||
471
src/main/java/tech/ailef/dbadmin/external/controller/DefaultDbAdminController.java
vendored
Normal file
471
src/main/java/tech/ailef/dbadmin/external/controller/DefaultDbAdminController.java
vendored
Normal file
@@ -0,0 +1,471 @@
|
||||
package tech.ailef.dbadmin.external.controller;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
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;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import tech.ailef.dbadmin.external.DbAdmin;
|
||||
import tech.ailef.dbadmin.external.DbAdminProperties;
|
||||
import tech.ailef.dbadmin.external.dbmapping.DbAdminRepository;
|
||||
import tech.ailef.dbadmin.external.dbmapping.DbObject;
|
||||
import tech.ailef.dbadmin.external.dbmapping.DbObjectSchema;
|
||||
import tech.ailef.dbadmin.external.dto.CompareOperator;
|
||||
import tech.ailef.dbadmin.external.dto.LogsSearchRequest;
|
||||
import tech.ailef.dbadmin.external.dto.PaginatedResult;
|
||||
import tech.ailef.dbadmin.external.dto.QueryFilter;
|
||||
import tech.ailef.dbadmin.external.exceptions.InvalidPageException;
|
||||
import tech.ailef.dbadmin.external.misc.Utils;
|
||||
import tech.ailef.dbadmin.internal.model.UserAction;
|
||||
import tech.ailef.dbadmin.internal.model.UserSetting;
|
||||
import tech.ailef.dbadmin.internal.repository.UserSettingsRepository;
|
||||
import tech.ailef.dbadmin.internal.service.UserActionService;
|
||||
|
||||
/**
|
||||
* The main DbAdmin controller that register most of the routes of the web interface.
|
||||
*/
|
||||
@Controller
|
||||
@RequestMapping(value= {"/${dbadmin.baseUrl}", "/${dbadmin.baseUrl}/"})
|
||||
public class DefaultDbAdminController {
|
||||
@Autowired
|
||||
private DbAdminProperties properties;
|
||||
|
||||
@Autowired
|
||||
private DbAdminRepository repository;
|
||||
|
||||
@Autowired
|
||||
private DbAdmin dbAdmin;
|
||||
|
||||
@Autowired
|
||||
private UserActionService userActionService;
|
||||
|
||||
|
||||
@Autowired
|
||||
private UserSettingsRepository userSettingsRepo;
|
||||
|
||||
/**
|
||||
* Home page with list of schemas
|
||||
* @param model
|
||||
* @param query
|
||||
* @return
|
||||
*/
|
||||
@GetMapping
|
||||
public String index(Model model, @RequestParam(required = false) String query) {
|
||||
|
||||
List<DbObjectSchema> schemas = dbAdmin.getSchemas();
|
||||
if (query != null && !query.isBlank()) {
|
||||
schemas = schemas.stream().filter(s -> {
|
||||
return s.getClassName().toLowerCase().contains(query.toLowerCase())
|
||||
|| s.getTableName().toLowerCase().contains(query.toLowerCase());
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
Map<String, Long> counts =
|
||||
schemas.stream().collect(Collectors.toMap(s -> s.getClassName(), s -> repository.count(s)));
|
||||
|
||||
model.addAttribute("schemas", schemas);
|
||||
model.addAttribute("query", query);
|
||||
model.addAttribute("counts", counts);
|
||||
model.addAttribute("activePage", "home");
|
||||
model.addAttribute("title", "Entities | Index");
|
||||
|
||||
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,
|
||||
@RequestParam(required=false) Integer pageSize, @RequestParam(required=false) String sortKey,
|
||||
@RequestParam(required=false) String sortOrder, @RequestParam MultiValueMap<String, String> otherParams,
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response) {
|
||||
|
||||
if (page == null) page = 1;
|
||||
if (pageSize == null) pageSize = 50;
|
||||
|
||||
Set<QueryFilter> queryFilters = Utils.computeFilters(otherParams);
|
||||
if (otherParams.containsKey("remove_field")) {
|
||||
List<String> fields = otherParams.get("remove_field");
|
||||
|
||||
for (int i = 0; i < fields.size(); i++) {
|
||||
QueryFilter toRemove =
|
||||
new QueryFilter(
|
||||
fields.get(i),
|
||||
CompareOperator.valueOf(otherParams.get("remove_op").get(i).toUpperCase()),
|
||||
otherParams.get("remove_value").get(i)
|
||||
);
|
||||
queryFilters.removeIf(f -> f.equals(toRemove));
|
||||
}
|
||||
|
||||
MultiValueMap<String, String> parameterMap = Utils.computeParams(queryFilters);
|
||||
|
||||
MultiValueMap<String, String> filteredParams = new LinkedMultiValueMap<>();
|
||||
request.getParameterMap().entrySet().stream()
|
||||
.filter(e -> !e.getKey().startsWith("remove_") && !e.getKey().startsWith("filter_"))
|
||||
.forEach(e -> {
|
||||
filteredParams.putIfAbsent(e.getKey(), new ArrayList<>());
|
||||
for (String v : e.getValue()) {
|
||||
if (filteredParams.get(e.getKey()).isEmpty()) {
|
||||
filteredParams.get(e.getKey()).add(v);
|
||||
} else {
|
||||
filteredParams.get(e.getKey()).set(0, v);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
filteredParams.putAll(parameterMap);
|
||||
String queryString = Utils.getQueryString(filteredParams);
|
||||
String redirectUrl = request.getServletPath() + queryString;
|
||||
return "redirect:" + redirectUrl.trim();
|
||||
}
|
||||
|
||||
DbObjectSchema schema = dbAdmin.findSchemaByClassName(className);
|
||||
|
||||
try {
|
||||
PaginatedResult<DbObject> result = null;
|
||||
if (query != null || !otherParams.isEmpty()) {
|
||||
result = repository.search(schema, query, page, pageSize, sortKey, sortOrder, queryFilters);
|
||||
} else {
|
||||
result = repository.findAll(schema, page, pageSize, sortKey, sortOrder);
|
||||
}
|
||||
|
||||
model.addAttribute("title", "Entities | " + schema.getJavaClass().getSimpleName() + " | Index");
|
||||
model.addAttribute("page", result);
|
||||
model.addAttribute("schema", schema);
|
||||
model.addAttribute("activePage", "entities");
|
||||
model.addAttribute("sortKey", sortKey);
|
||||
model.addAttribute("query", query);
|
||||
model.addAttribute("sortOrder", sortOrder);
|
||||
model.addAttribute("activeFilters", queryFilters);
|
||||
return "model/list";
|
||||
|
||||
} catch (InvalidPageException e) {
|
||||
return "redirect:/" + properties.getBaseUrl() + "/model/" + className;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
model.addAttribute("activePage", "entities");
|
||||
model.addAttribute("schema", schema);
|
||||
|
||||
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);
|
||||
|
||||
DbObject object = repository.findById(schema, id).orElseThrow(() -> {
|
||||
return new ResponseStatusException(
|
||||
HttpStatus.NOT_FOUND, "Object " + className + " with id " + id + " not found"
|
||||
);
|
||||
});
|
||||
|
||||
model.addAttribute("title", "Entities | " + schema.getJavaClass().getSimpleName() + " | " + object.getDisplayName());
|
||||
model.addAttribute("object", object);
|
||||
model.addAttribute("activePage", "entities");
|
||||
model.addAttribute("schema", schema);
|
||||
|
||||
return "model/show";
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/model/{className}/create")
|
||||
public String create(Model model, @PathVariable String className) {
|
||||
DbObjectSchema schema = dbAdmin.findSchemaByClassName(className);
|
||||
|
||||
model.addAttribute("className", className);
|
||||
model.addAttribute("schema", schema);
|
||||
model.addAttribute("title", "Entities | " + schema.getJavaClass().getSimpleName() + " | Create");
|
||||
model.addAttribute("activePage", "entities");
|
||||
model.addAttribute("create", true);
|
||||
|
||||
return "model/create";
|
||||
}
|
||||
|
||||
@GetMapping("/model/{className}/edit/{id}")
|
||||
public String edit(Model model, @PathVariable String className, @PathVariable String id) {
|
||||
DbObjectSchema schema = dbAdmin.findSchemaByClassName(className);
|
||||
|
||||
DbObject object = repository.findById(schema, id).orElseThrow(() -> {
|
||||
return new ResponseStatusException(
|
||||
HttpStatus.NOT_FOUND, "Object " + className + " with id " + id + " not found"
|
||||
);
|
||||
});
|
||||
|
||||
model.addAttribute("title", "Entities | " + schema.getJavaClass().getSimpleName() + " | Edit | " + object.getDisplayName());
|
||||
model.addAttribute("className", className);
|
||||
model.addAttribute("object", object);
|
||||
model.addAttribute("schema", schema);
|
||||
model.addAttribute("activePage", "entities");
|
||||
model.addAttribute("create", false);
|
||||
|
||||
return "model/create";
|
||||
}
|
||||
|
||||
@PostMapping(value="/model/{className}/delete/{id}")
|
||||
/**
|
||||
* Delete a single row based on its primary key value
|
||||
* @param className
|
||||
* @param id
|
||||
* @param attr
|
||||
* @return
|
||||
*/
|
||||
public String delete(@PathVariable String className, @PathVariable String id, RedirectAttributes attr) {
|
||||
DbObjectSchema schema = dbAdmin.findSchemaByClassName(className);
|
||||
|
||||
try {
|
||||
repository.delete(schema, id);
|
||||
} catch (DataIntegrityViolationException e) {
|
||||
attr.addFlashAttribute("errorTitle", "Unable to DELETE row");
|
||||
attr.addFlashAttribute("error", e.getMessage());
|
||||
}
|
||||
|
||||
saveAction(new UserAction(schema.getTableName(), id, "DELETE"));
|
||||
|
||||
return "redirect:/" + properties.getBaseUrl() + "/model/" + className;
|
||||
}
|
||||
|
||||
@PostMapping(value="/model/{className}/delete")
|
||||
/**
|
||||
* Delete multiple rows based on their primary key values
|
||||
* @param className
|
||||
* @param ids
|
||||
* @param attr
|
||||
* @return
|
||||
*/
|
||||
public String delete(@PathVariable String className, @RequestParam String[] ids, RedirectAttributes attr) {
|
||||
DbObjectSchema schema = dbAdmin.findSchemaByClassName(className);
|
||||
|
||||
int countDeleted = 0;
|
||||
for (String id : ids) {
|
||||
try {
|
||||
repository.delete(schema, id);
|
||||
countDeleted += 1;
|
||||
} catch (DataIntegrityViolationException e) {
|
||||
attr.addFlashAttribute("error", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (countDeleted > 0)
|
||||
attr.addFlashAttribute("message", "Deleted " + countDeleted + " of " + ids.length + " items");
|
||||
|
||||
for (String id : ids) {
|
||||
saveAction(new UserAction(schema.getTableName(), id, "DELETE"));
|
||||
}
|
||||
|
||||
return "redirect:/" + properties.getBaseUrl() + "/model/" + className;
|
||||
}
|
||||
|
||||
@PostMapping(value="/model/{className}/create")
|
||||
public String store(@PathVariable String className,
|
||||
@RequestParam MultiValueMap<String, String> formParams,
|
||||
@RequestParam Map<String, MultipartFile> files,
|
||||
RedirectAttributes attr) {
|
||||
// Extract all parameters that have exactly 1 value,
|
||||
// as these will be the raw values for the object that is being
|
||||
// created.
|
||||
// The remaining parmeters which have more than 1 value
|
||||
// are IDs in a many-to-many relationship and need to be
|
||||
// handled separately
|
||||
Map<String, String> params = new HashMap<>();
|
||||
for (String param : formParams.keySet()) {
|
||||
if (!param.endsWith("[]")) {
|
||||
params.put(param, formParams.getFirst(param));
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, List<String>> multiValuedParams = new HashMap<>();
|
||||
for (String param : formParams.keySet()) {
|
||||
if (param.endsWith("[]")) {
|
||||
List<String> list = formParams.get(param);
|
||||
// If the request contains only 1 parameter value, it's the empty
|
||||
// value that signifies just the presence of the field (e.g. the
|
||||
// user might've deleted all the value)
|
||||
if (list.size() == 1) {
|
||||
multiValuedParams.put(param, new ArrayList<>());
|
||||
} else {
|
||||
list.removeIf(f -> f.isBlank());
|
||||
multiValuedParams.put(
|
||||
param,
|
||||
list
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String c = params.get("__dbadmin_create");
|
||||
if (c == null) {
|
||||
throw new ResponseStatusException(
|
||||
HttpStatus.INTERNAL_SERVER_ERROR, "Missing required param __dbadmin_create"
|
||||
);
|
||||
}
|
||||
|
||||
boolean create = Boolean.parseBoolean(c);
|
||||
|
||||
DbObjectSchema schema = dbAdmin.findSchemaByClassName(className);
|
||||
|
||||
String pkValue = params.get(schema.getPrimaryKey().getName());
|
||||
if (pkValue == null || pkValue.isBlank()) {
|
||||
pkValue = null;
|
||||
}
|
||||
|
||||
if (pkValue == null) {
|
||||
try {
|
||||
Object newPrimaryKey = repository.create(schema, params, files, pkValue);
|
||||
repository.attachManyToMany(schema, newPrimaryKey, multiValuedParams);
|
||||
pkValue = newPrimaryKey.toString();
|
||||
attr.addFlashAttribute("message", "Item created successfully.");
|
||||
saveAction(new UserAction(schema.getTableName(), pkValue, "CREATE"));
|
||||
} catch (DataIntegrityViolationException 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());
|
||||
attr.addFlashAttribute("params", params);
|
||||
}
|
||||
|
||||
} else {
|
||||
Optional<DbObject> object = repository.findById(schema, pkValue);
|
||||
|
||||
if (!object.isEmpty()) {
|
||||
if (create) {
|
||||
attr.addFlashAttribute("errorTitle", "Unable to create item");
|
||||
attr.addFlashAttribute("error", "Item with id " + object.get().getPrimaryKeyValue() + " already exists.");
|
||||
attr.addFlashAttribute("params", params);
|
||||
} else {
|
||||
try {
|
||||
repository.update(schema, params, files);
|
||||
repository.attachManyToMany(schema, pkValue, multiValuedParams);
|
||||
attr.addFlashAttribute("message", "Item saved successfully.");
|
||||
saveAction(new UserAction(schema.getTableName(), pkValue, "EDIT"));
|
||||
} catch (DataIntegrityViolationException 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());
|
||||
attr.addFlashAttribute("params", params);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
Object newPrimaryKey = repository.create(schema, params, files, pkValue);
|
||||
repository.attachManyToMany(schema, newPrimaryKey, multiValuedParams);
|
||||
attr.addFlashAttribute("message", "Item created successfully");
|
||||
saveAction(new UserAction(schema.getTableName(), pkValue, "CREATE"));
|
||||
} catch (DataIntegrityViolationException e) {
|
||||
attr.addFlashAttribute("errorTitle", "Unable to INSERT row (no changes applied)");
|
||||
attr.addFlashAttribute("error", e.getMessage());
|
||||
attr.addFlashAttribute("params", params);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (attr.getFlashAttributes().containsKey("error")) {
|
||||
if (create)
|
||||
return "redirect:/" + properties.getBaseUrl() + "/model/" + schema.getClassName() + "/create";
|
||||
else
|
||||
return "redirect:/" + properties.getBaseUrl() + "/model/" + schema.getClassName() + "/edit/" + pkValue;
|
||||
} else {
|
||||
return "redirect:/" + properties.getBaseUrl() + "/model/" + schema.getClassName() + "/show/" + pkValue;
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/logs")
|
||||
public String logs(Model model, LogsSearchRequest searchRequest) {
|
||||
model.addAttribute("activePage", "logs");
|
||||
model.addAttribute(
|
||||
"page",
|
||||
userActionService.findActions(
|
||||
searchRequest.getTable(),
|
||||
searchRequest.getActionType(),
|
||||
searchRequest.getItemId(),
|
||||
searchRequest.toPageRequest()
|
||||
)
|
||||
);
|
||||
model.addAttribute("schemas", dbAdmin.getSchemas());
|
||||
model.addAttribute("searchRequest", searchRequest);
|
||||
return "logs";
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/settings")
|
||||
public String settings(Model model) {
|
||||
model.addAttribute("activePage", "settings");
|
||||
return "settings";
|
||||
}
|
||||
|
||||
@PostMapping("/settings")
|
||||
public String settings(@RequestParam Map<String, String> params, Model model) {
|
||||
for (String paramName : params.keySet()) {
|
||||
userSettingsRepo.save(new UserSetting(paramName, params.get(paramName)));
|
||||
}
|
||||
model.addAttribute("activePage", "settings");
|
||||
return "settings";
|
||||
}
|
||||
|
||||
private UserAction saveAction(UserAction action) {
|
||||
return userActionService.save(action);
|
||||
}
|
||||
}
|
||||
104
src/main/java/tech/ailef/dbadmin/external/controller/DownloadController.java
vendored
Normal file
104
src/main/java/tech/ailef/dbadmin/external/controller/DownloadController.java
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
package tech.ailef.dbadmin.external.controller;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.apache.tika.Tika;
|
||||
import org.apache.tika.mime.MimeTypeException;
|
||||
import org.apache.tika.mime.MimeTypes;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import tech.ailef.dbadmin.external.DbAdmin;
|
||||
import tech.ailef.dbadmin.external.dbmapping.DbAdminRepository;
|
||||
import tech.ailef.dbadmin.external.dbmapping.DbFieldValue;
|
||||
import tech.ailef.dbadmin.external.dbmapping.DbObject;
|
||||
import tech.ailef.dbadmin.external.dbmapping.DbObjectSchema;
|
||||
import tech.ailef.dbadmin.external.exceptions.DbAdminException;
|
||||
|
||||
/**
|
||||
* Controller to serve file or images (`@DisplayImage`)
|
||||
*/
|
||||
@Controller
|
||||
@RequestMapping(value = {"/${dbadmin.baseUrl}/download", "/${dbadmin.baseUrl}/download/"})
|
||||
public class DownloadController {
|
||||
@Autowired
|
||||
private DbAdminRepository repository;
|
||||
|
||||
@Autowired
|
||||
private DbAdmin dbAdmin;
|
||||
|
||||
|
||||
@GetMapping(value="/{className}/{fieldName}/{id}/image", produces = MediaType.IMAGE_JPEG_VALUE)
|
||||
@ResponseBody
|
||||
public ResponseEntity<byte[]> serveImage(@PathVariable String className,
|
||||
@PathVariable String fieldName, @PathVariable String id) {
|
||||
|
||||
DbObjectSchema schema = dbAdmin.findSchemaByClassName(className);
|
||||
|
||||
Optional<DbObject> object = repository.findById(schema, id);
|
||||
|
||||
if (object.isPresent()) {
|
||||
DbObject dbObject = object.get();
|
||||
DbFieldValue dbFieldValue = dbObject.get(fieldName);
|
||||
byte[] file = (byte[])dbFieldValue.getValue();
|
||||
return ResponseEntity.ok(file);
|
||||
} else {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Object with id " + id + " not found");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@GetMapping("/{className}/{fieldName}/{id}")
|
||||
@ResponseBody
|
||||
public ResponseEntity<byte[]> serveFile(@PathVariable String className,
|
||||
@PathVariable String fieldName, @PathVariable String id) {
|
||||
|
||||
DbObjectSchema schema = dbAdmin.findSchemaByClassName(className);
|
||||
|
||||
Optional<DbObject> object = repository.findById(schema, id);
|
||||
|
||||
if (object.isPresent()) {
|
||||
DbObject dbObject = object.get();
|
||||
|
||||
DbFieldValue dbFieldValue;
|
||||
try {
|
||||
dbFieldValue = dbObject.get(fieldName);
|
||||
} catch (DbAdminException e) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Field not found", e);
|
||||
}
|
||||
|
||||
if (dbFieldValue.getValue() == null) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "There's no file attached to this item");
|
||||
}
|
||||
|
||||
byte[] file = (byte[])dbFieldValue.getValue();
|
||||
|
||||
String filename = schema.getClassName() + "_" + id + "_" + fieldName;
|
||||
try {
|
||||
Tika tika = new Tika();
|
||||
String detect = tika.detect(file);
|
||||
String ext = MimeTypes.getDefaultMimeTypes().forName(detect).getExtension();
|
||||
filename = filename + ext;
|
||||
} catch (MimeTypeException e) {
|
||||
// Unable to determine extension, leave as is
|
||||
}
|
||||
|
||||
return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,
|
||||
"attachment; filename=\"" + filename + "\"").body(file);
|
||||
} else {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Object with id " + id + " not found");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
57
src/main/java/tech/ailef/dbadmin/external/controller/GlobalController.java
vendored
Normal file
57
src/main/java/tech/ailef/dbadmin/external/controller/GlobalController.java
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
package tech.ailef.dbadmin.external.controller;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import tech.ailef.dbadmin.external.DbAdminProperties;
|
||||
import tech.ailef.dbadmin.internal.UserConfiguration;
|
||||
|
||||
/**
|
||||
* This class registers some ModelAttribute objects that are
|
||||
* used in all templates.
|
||||
*/
|
||||
@ControllerAdvice
|
||||
public class GlobalController {
|
||||
|
||||
@Autowired
|
||||
private DbAdminProperties props;
|
||||
|
||||
@Autowired
|
||||
private UserConfiguration userConf;
|
||||
|
||||
/**
|
||||
* A multi valued map containing the query parameters. It is used primarily
|
||||
* in building complex URL when performing faceted search with multiple filters.
|
||||
* @param request the incoming request
|
||||
* @return multi valued map of request parameters
|
||||
*/
|
||||
@ModelAttribute("queryParams")
|
||||
public Map<String, String[]> getQueryParams(HttpServletRequest request) {
|
||||
return request.getParameterMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* The baseUrl as specified in the properties file by the user
|
||||
* @param request
|
||||
* @return
|
||||
*/
|
||||
@ModelAttribute("baseUrl")
|
||||
public String getBaseUrl() {
|
||||
return props.getBaseUrl();
|
||||
}
|
||||
|
||||
@ModelAttribute("requestUrl")
|
||||
public String getRequestUrl(HttpServletRequest request) {
|
||||
return request.getRequestURI();
|
||||
}
|
||||
|
||||
@ModelAttribute("userConf")
|
||||
public UserConfiguration getUserConf() {
|
||||
return userConf;
|
||||
}
|
||||
}
|
||||
|
||||
41
src/main/java/tech/ailef/dbadmin/external/controller/rest/AutocompleteController.java
vendored
Normal file
41
src/main/java/tech/ailef/dbadmin/external/controller/rest/AutocompleteController.java
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
package tech.ailef.dbadmin.external.controller.rest;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import tech.ailef.dbadmin.external.DbAdmin;
|
||||
import tech.ailef.dbadmin.external.dbmapping.DbAdminRepository;
|
||||
import tech.ailef.dbadmin.external.dbmapping.DbObjectSchema;
|
||||
import tech.ailef.dbadmin.external.dto.AutocompleteSearchResult;
|
||||
|
||||
/**
|
||||
* API controller for autocomplete results
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(value= {"/${dbadmin.baseUrl}/api/autocomplete", "/${dbadmin.baseUrl}/api/autocomplete/"})
|
||||
public class AutocompleteController {
|
||||
@Autowired
|
||||
private DbAdmin dbAdmin;
|
||||
|
||||
@Autowired
|
||||
private DbAdminRepository repository;
|
||||
|
||||
@GetMapping("/{className}")
|
||||
public ResponseEntity<?> autocomplete(@PathVariable String className, @RequestParam String query) {
|
||||
DbObjectSchema schema = dbAdmin.findSchemaByClassName(className);
|
||||
|
||||
List<AutocompleteSearchResult> search = repository.search(schema, query)
|
||||
.stream().map(x -> new AutocompleteSearchResult(x))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return ResponseEntity.ok(search);
|
||||
}
|
||||
}
|
||||
106
src/main/java/tech/ailef/dbadmin/external/controller/rest/DefaultDbAdminRestController.java
vendored
Normal file
106
src/main/java/tech/ailef/dbadmin/external/controller/rest/DefaultDbAdminRestController.java
vendored
Normal file
@@ -0,0 +1,106 @@
|
||||
package tech.ailef.dbadmin.external.controller.rest;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import tech.ailef.dbadmin.external.DbAdmin;
|
||||
import tech.ailef.dbadmin.external.DbAdminProperties;
|
||||
import tech.ailef.dbadmin.external.dbmapping.DbAdminRepository;
|
||||
import tech.ailef.dbadmin.external.dbmapping.DbObjectSchema;
|
||||
import tech.ailef.dbadmin.external.dto.PaginatedResult;
|
||||
import tech.ailef.dbadmin.external.exceptions.DbAdminException;
|
||||
|
||||
@RestController
|
||||
@RequestMapping(value = {"/${dbadmin.baseUrl}/api", "/${dbadmin.baseUrl}/api/"})
|
||||
public class DefaultDbAdminRestController {
|
||||
@Autowired
|
||||
public DbAdmin dbAdmin;
|
||||
|
||||
@Autowired
|
||||
private DbAdminProperties properties;
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
// @Autowired
|
||||
// @Qualifier("internalJdbc")
|
||||
// private JdbcTemplate internalJdbc;
|
||||
|
||||
// @GetMapping("/configuration")
|
||||
// public ResponseEntity<?> conf() {
|
||||
// return ResponseEntity.ok(properties.toMap());
|
||||
// }
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<?> index(@RequestParam(required = false) String query) {
|
||||
checkInit();
|
||||
|
||||
List<DbObjectSchema> schemas = dbAdmin.getSchemas();
|
||||
if (query != null && !query.isBlank()) {
|
||||
schemas = schemas.stream().filter(s -> {
|
||||
return s.getClassName().toLowerCase().contains(query.toLowerCase())
|
||||
|| s.getTableName().toLowerCase().contains(query.toLowerCase());
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(schemas);
|
||||
}
|
||||
|
||||
@GetMapping("/model/{className}")
|
||||
public ResponseEntity<?> list(@PathVariable String className,
|
||||
@RequestParam(required=false) Integer page, @RequestParam(required=false) Integer pageSize,
|
||||
@RequestParam(required=false) String sortKey, @RequestParam(required=false) String sortOrder) {
|
||||
checkInit();
|
||||
DbAdminRepository repository = new DbAdminRepository(jdbcTemplate);
|
||||
|
||||
if (page == null) page = 1;
|
||||
if (pageSize == null) pageSize = 50;
|
||||
|
||||
DbObjectSchema schema = dbAdmin.findSchemaByClassName(className);
|
||||
PaginatedResult result = repository.findAll(schema, page, pageSize, sortKey, sortOrder);
|
||||
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@GetMapping("/model/{className}/schema")
|
||||
public ResponseEntity<?> schema(@PathVariable String className) {
|
||||
checkInit();
|
||||
|
||||
DbObjectSchema schema = dbAdmin.findSchemaByClassName(className);
|
||||
|
||||
return ResponseEntity.ok(schema);
|
||||
}
|
||||
|
||||
// @GetMapping("/model/{className}/show/{id}")
|
||||
// public ResponseEntity<?> show(@PathVariable String className, @PathVariable String id,
|
||||
// @RequestParam(required = false) Boolean expand) {
|
||||
// checkInit();
|
||||
// DbAdminRepository repository = new DbAdminRepository(jdbcTemplate);
|
||||
// if (expand == null) expand = true;
|
||||
//
|
||||
// DbObjectSchema schema = dbAdmin.findSchemaByClassName(className);
|
||||
//
|
||||
// DbObject object = repository.findById(schema, id).orElseThrow(() -> {
|
||||
// return new ResponseStatusException(
|
||||
// HttpStatus.NOT_FOUND, "Object " + className + " with id " + id + " not found"
|
||||
// );
|
||||
// });
|
||||
//
|
||||
// return ResponseEntity.ok(new DbObjectDTO(object, expand));
|
||||
// }
|
||||
|
||||
private void checkInit() {
|
||||
if (dbAdmin == null)
|
||||
throw new DbAdminException("Not initialized correctly: DB_ADMIN object is null.");
|
||||
}
|
||||
}
|
||||
199
src/main/java/tech/ailef/dbadmin/external/dbmapping/AdvancedJpaRepository.java
vendored
Normal file
199
src/main/java/tech/ailef/dbadmin/external/dbmapping/AdvancedJpaRepository.java
vendored
Normal file
@@ -0,0 +1,199 @@
|
||||
package tech.ailef.dbadmin.external.dbmapping;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.Query;
|
||||
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||
import jakarta.persistence.criteria.CriteriaQuery;
|
||||
import jakarta.persistence.criteria.CriteriaUpdate;
|
||||
import jakarta.persistence.criteria.Path;
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
import jakarta.persistence.criteria.Root;
|
||||
import tech.ailef.dbadmin.external.dto.CompareOperator;
|
||||
import tech.ailef.dbadmin.external.dto.QueryFilter;
|
||||
import tech.ailef.dbadmin.external.exceptions.DbAdminException;
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
public class AdvancedJpaRepository extends SimpleJpaRepository {
|
||||
|
||||
private EntityManager entityManager;
|
||||
|
||||
private DbObjectSchema schema;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public AdvancedJpaRepository(DbObjectSchema schema, EntityManager em) {
|
||||
super(schema.getJavaClass(), em);
|
||||
this.entityManager = em;
|
||||
this.schema = schema;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public long count(String q, Set<QueryFilter> queryFilters) {
|
||||
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
|
||||
CriteriaQuery query = cb.createQuery(Long.class);
|
||||
Root root = query.from(schema.getJavaClass());
|
||||
|
||||
List<Predicate> finalPredicates = buildPredicates(q, queryFilters, cb, root);
|
||||
|
||||
query.select(cb.count(root.get(schema.getPrimaryKey().getName())))
|
||||
.where(
|
||||
cb.and(
|
||||
finalPredicates.toArray(new Predicate[finalPredicates.size()])
|
||||
)
|
||||
);
|
||||
|
||||
Object o = entityManager.createQuery(query).getSingleResult();
|
||||
return (Long)o;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<Object> search(String q, int page, int pageSize, String sortKey, String sortOrder, Set<QueryFilter> filters) {
|
||||
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
|
||||
CriteriaQuery query = cb.createQuery(schema.getJavaClass());
|
||||
Root root = query.from(schema.getJavaClass());
|
||||
|
||||
List<Predicate> finalPredicates = buildPredicates(q, filters, cb, root);
|
||||
|
||||
query.select(root)
|
||||
.where(
|
||||
cb.and(
|
||||
finalPredicates.toArray(new Predicate[finalPredicates.size()]) // query search on String fields
|
||||
)
|
||||
|
||||
);
|
||||
if (sortKey != null)
|
||||
query.orderBy(sortOrder.equals("DESC") ? cb.desc(root.get(sortKey)) : cb.asc(root.get(sortKey)));
|
||||
|
||||
return entityManager.createQuery(query).setMaxResults(pageSize)
|
||||
.setFirstResult((page - 1) * pageSize).getResultList();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<Predicate> buildPredicates(String q, Set<QueryFilter> queryFilters,
|
||||
CriteriaBuilder cb, Path root) {
|
||||
List<Predicate> finalPredicates = new ArrayList<>();
|
||||
|
||||
List<DbField> stringFields =
|
||||
schema.getSortedFields().stream().filter(f -> f.getType() == DbFieldType.STRING)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<Predicate> queryPredicates = new ArrayList<>();
|
||||
if (q != null) {
|
||||
for (DbField f : stringFields) {
|
||||
Path path = root.get(f.getJavaName());
|
||||
queryPredicates.add(cb.like(cb.lower(cb.toString(path)), "%" + q.toLowerCase() + "%"));
|
||||
}
|
||||
|
||||
Predicate queryPredicate = cb.or(queryPredicates.toArray(new Predicate[queryPredicates.size()]));
|
||||
finalPredicates.add(queryPredicate);
|
||||
}
|
||||
|
||||
|
||||
if (queryFilters == null) queryFilters = new HashSet<>();
|
||||
for (QueryFilter filter : queryFilters) {
|
||||
CompareOperator op = filter.getOp();
|
||||
String field = filter.getField();
|
||||
String v = filter.getValue();
|
||||
|
||||
DbField dbField = schema.getFieldByJavaName(field);
|
||||
Object value = dbField.getType().parseValue(v);
|
||||
|
||||
if (op == CompareOperator.STRING_EQ) {
|
||||
finalPredicates.add(cb.equal(cb.lower(cb.toString(root.get(field))), value.toString().toLowerCase()));
|
||||
} else if (op == CompareOperator.CONTAINS) {
|
||||
finalPredicates.add(
|
||||
cb.like(cb.lower(cb.toString(root.get(field))), "%" + value.toString().toLowerCase() + "%")
|
||||
);
|
||||
} else if (op == CompareOperator.EQ) {
|
||||
finalPredicates.add(
|
||||
cb.equal(root.get(field), value)
|
||||
);
|
||||
} else if (op == CompareOperator.GT) {
|
||||
finalPredicates.add(
|
||||
cb.greaterThan(root.get(field), value.toString())
|
||||
);
|
||||
} else if (op == CompareOperator.LT) {
|
||||
finalPredicates.add(
|
||||
cb.lessThan(root.get(field), value.toString())
|
||||
);
|
||||
} else if (op == CompareOperator.AFTER) {
|
||||
if (value instanceof LocalDate)
|
||||
finalPredicates.add(
|
||||
cb.greaterThan(root.get(field), (LocalDate)value)
|
||||
);
|
||||
else if (value instanceof LocalDateTime)
|
||||
finalPredicates.add(
|
||||
cb.greaterThan(root.get(field), (LocalDateTime)value)
|
||||
);
|
||||
|
||||
} else if (op == CompareOperator.BEFORE) {
|
||||
if (value instanceof LocalDate)
|
||||
finalPredicates.add(
|
||||
cb.lessThan(root.get(field), (LocalDate)value)
|
||||
);
|
||||
else if (value instanceof LocalDateTime)
|
||||
finalPredicates.add(
|
||||
cb.lessThan(root.get(field), (LocalDateTime)value)
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
return finalPredicates;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public int update(DbObjectSchema schema, Map<String, String> params, Map<String, MultipartFile> 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();
|
||||
}
|
||||
}
|
||||
264
src/main/java/tech/ailef/dbadmin/external/dbmapping/DbAdminRepository.java
vendored
Normal file
264
src/main/java/tech/ailef/dbadmin/external/dbmapping/DbAdminRepository.java
vendored
Normal file
@@ -0,0 +1,264 @@
|
||||
package tech.ailef.dbadmin.external.dbmapping;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import tech.ailef.dbadmin.external.dto.PaginatedResult;
|
||||
import tech.ailef.dbadmin.external.dto.PaginationInfo;
|
||||
import tech.ailef.dbadmin.external.dto.QueryFilter;
|
||||
import tech.ailef.dbadmin.external.exceptions.DbAdminException;
|
||||
import tech.ailef.dbadmin.external.exceptions.InvalidPageException;
|
||||
|
||||
/**
|
||||
* Implements the basic CRUD operations (and some more)
|
||||
*/
|
||||
@Component
|
||||
public class DbAdminRepository {
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
public DbAdminRepository(JdbcTemplate jdbcTemplate) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an object by ID
|
||||
* @param schema the schema where to look
|
||||
* @param id the primary key value
|
||||
* @return an optional with the object with the specified primary key value
|
||||
*/
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
public Optional<DbObject> findById(DbObjectSchema schema, Object id) {
|
||||
SimpleJpaRepository repository = schema.getJpaRepository();
|
||||
|
||||
Optional optional = repository.findById(id);
|
||||
if (optional.isEmpty())
|
||||
return Optional.empty();
|
||||
else
|
||||
return Optional.of(new DbObject(optional.get(), schema));
|
||||
}
|
||||
|
||||
public long count(DbObjectSchema schema) {
|
||||
return schema.getJpaRepository().count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the elements that match the fuzzy search
|
||||
* @param schema
|
||||
* @param query
|
||||
* @return
|
||||
*/
|
||||
public long count(DbObjectSchema schema, String query, Set<QueryFilter> queryFilters) {
|
||||
return schema.getJpaRepository().count(query, queryFilters);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Find all the objects in the schema. Only returns a single page of
|
||||
* results based on the input parameters.
|
||||
* @param schema
|
||||
* @param page
|
||||
* @param pageSize
|
||||
* @param sortKey
|
||||
* @param sortOrder
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
public PaginatedResult findAll(DbObjectSchema schema, int page, int pageSize, String sortKey, String sortOrder) {
|
||||
SimpleJpaRepository repository = schema.getJpaRepository();
|
||||
|
||||
long maxElement = count(schema);
|
||||
int maxPage = (int)(Math.ceil ((double)maxElement / pageSize));
|
||||
|
||||
if (page <= 0) page = 1;
|
||||
if (page > maxPage && maxPage != 0) {
|
||||
throw new InvalidPageException();
|
||||
}
|
||||
|
||||
Sort sort = null;
|
||||
if (sortKey != null) {
|
||||
sort = Sort.by(sortKey);
|
||||
}
|
||||
if (Objects.equals(sortOrder, "ASC")) {
|
||||
sort = sort.ascending();
|
||||
} else if (Objects.equals(sortOrder, "DESC")) {
|
||||
sort = sort.descending();
|
||||
}
|
||||
PageRequest pageRequestion = null;
|
||||
|
||||
if (sort != null) {
|
||||
pageRequestion = PageRequest.of(page - 1, pageSize, sort);
|
||||
} else {
|
||||
pageRequestion = PageRequest.of(page - 1, pageSize);
|
||||
}
|
||||
|
||||
|
||||
Page findAll = repository.findAll(pageRequestion);
|
||||
List<DbObject> results = new ArrayList<>();
|
||||
for (Object o : findAll) {
|
||||
results.add(new DbObject(o, schema));
|
||||
}
|
||||
|
||||
|
||||
return new PaginatedResult<DbObject>(
|
||||
new PaginationInfo(page, maxPage, pageSize, maxElement, null, new HashSet<>()),
|
||||
results
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing object with new values
|
||||
* @param schema
|
||||
* @param params
|
||||
*/
|
||||
@Transactional("transactionManager")
|
||||
public void update(DbObjectSchema schema, Map<String, String> params, Map<String, MultipartFile> files) {
|
||||
schema.getJpaRepository().update(schema, params, files);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Transactional("transactionManager")
|
||||
private void save(DbObjectSchema schema, DbObject o) {
|
||||
schema.getJpaRepository().save(o.getUnderlyingInstance());
|
||||
}
|
||||
|
||||
@Transactional("transactionManager")
|
||||
public void attachManyToMany(DbObjectSchema schema, Object id, Map<String, List<String>> params) {
|
||||
Optional<DbObject> optional = findById(schema, id);
|
||||
|
||||
DbObject dbObject = optional.orElseThrow(() -> {
|
||||
return new DbAdminException("Unable to retrieve newly inserted item");
|
||||
});
|
||||
|
||||
for (String mParam : params.keySet()) {
|
||||
String fieldName = mParam.replace("[]", "");
|
||||
|
||||
List<String> idValues = params.get(mParam);
|
||||
DbField field = schema.getFieldByName(fieldName);
|
||||
|
||||
DbObjectSchema linkedSchema = field.getConnectedSchema();
|
||||
|
||||
List<DbObject> traverseMany = new ArrayList<>();
|
||||
for (String oId : idValues) {
|
||||
Optional<DbObject> findById = findById(linkedSchema, oId);
|
||||
if (findById.isPresent()) {
|
||||
traverseMany.add(findById.get());
|
||||
}
|
||||
}
|
||||
|
||||
dbObject.set(
|
||||
fieldName,
|
||||
traverseMany.stream().map(o -> o.getUnderlyingInstance()).collect(Collectors.toList())
|
||||
);
|
||||
}
|
||||
|
||||
save(schema, dbObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new object with the specific primary key and values,
|
||||
* returns the primary key of the created object
|
||||
* @param schema
|
||||
* @param values
|
||||
* @param primaryKey
|
||||
*/
|
||||
public Object create(DbObjectSchema schema, Map<String, String> values, Map<String, MultipartFile> files, String primaryKey) {
|
||||
SimpleJdbcInsert insert = new SimpleJdbcInsert(jdbcTemplate).withTableName(schema.getTableName());
|
||||
|
||||
Map<String, Object> allValues = new HashMap<>();
|
||||
allValues.putAll(values);
|
||||
|
||||
values.keySet().forEach(fieldName -> {
|
||||
if (values.get(fieldName).isBlank()) {
|
||||
allValues.put(fieldName, null);
|
||||
}
|
||||
});
|
||||
|
||||
files.keySet().forEach(f -> {
|
||||
try {
|
||||
allValues.put(f, files.get(f).getBytes());
|
||||
} catch (IOException e) {
|
||||
throw new DbAdminException(e);
|
||||
}
|
||||
});
|
||||
|
||||
if (primaryKey == null) {
|
||||
insert = insert.usingGeneratedKeyColumns(schema.getPrimaryKey().getName());
|
||||
return insert.executeAndReturnKey(allValues);
|
||||
} else {
|
||||
insert.execute(allValues);
|
||||
return primaryKey;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fuzzy search on primary key value and display name
|
||||
* @param schema
|
||||
* @param query
|
||||
* @return
|
||||
*/
|
||||
public PaginatedResult<DbObject> search(DbObjectSchema schema, String query, int page, int pageSize, String sortKey,
|
||||
String sortOrder, Set<QueryFilter> queryFilters) {
|
||||
AdvancedJpaRepository jpaRepository = schema.getJpaRepository();
|
||||
|
||||
long maxElement = count(schema, query, queryFilters);
|
||||
int maxPage = (int)(Math.ceil ((double)maxElement / pageSize));
|
||||
|
||||
if (page <= 0) page = 1;
|
||||
if (page > maxPage && maxPage != 0) {
|
||||
throw new InvalidPageException();
|
||||
}
|
||||
|
||||
return new PaginatedResult<DbObject>(
|
||||
new PaginationInfo(page, maxPage, pageSize, maxElement, query, queryFilters),
|
||||
jpaRepository.search(query, page, pageSize, sortKey, sortOrder, queryFilters).stream()
|
||||
.map(o -> new DbObject(o, schema))
|
||||
.toList()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fuzzy search on primary key value and display name
|
||||
* @param schema
|
||||
* @param query
|
||||
* @return
|
||||
*/
|
||||
public List<DbObject> search(DbObjectSchema schema, String query) {
|
||||
AdvancedJpaRepository jpaRepository = schema.getJpaRepository();
|
||||
|
||||
return jpaRepository.search(query, 1, 50, null, null, null).stream()
|
||||
.map(o -> new DbObject(o, schema))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a specific object
|
||||
* @param schema
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@Transactional("transactionManager")
|
||||
public void delete(DbObjectSchema schema, String id) {
|
||||
schema.getJpaRepository().deleteById(id);
|
||||
}
|
||||
|
||||
}
|
||||
136
src/main/java/tech/ailef/dbadmin/external/dbmapping/DbField.java
vendored
Normal file
136
src/main/java/tech/ailef/dbadmin/external/dbmapping/DbField.java
vendored
Normal file
@@ -0,0 +1,136 @@
|
||||
package tech.ailef.dbadmin.external.dbmapping;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
import tech.ailef.dbadmin.external.annotations.DisplayImage;
|
||||
|
||||
public class DbField {
|
||||
protected String dbName;
|
||||
|
||||
protected String javaName;
|
||||
|
||||
protected DbFieldType type;
|
||||
|
||||
@JsonIgnore
|
||||
protected Field field;
|
||||
|
||||
/**
|
||||
* If this field is a foreign key, the class of the
|
||||
* entity that is connected to it
|
||||
*/
|
||||
@JsonIgnore
|
||||
private Class<?> connectedType;
|
||||
|
||||
private boolean primaryKey;
|
||||
|
||||
private boolean nullable;
|
||||
|
||||
private String format;
|
||||
|
||||
@JsonIgnore
|
||||
private DbObjectSchema schema;
|
||||
|
||||
public DbField(String javaName, String name, Field field, DbFieldType type, DbObjectSchema schema, String format) {
|
||||
this.javaName = javaName;
|
||||
this.dbName = name;
|
||||
this.schema = schema;
|
||||
this.field = field;
|
||||
this.type = type;
|
||||
this.format = format;
|
||||
}
|
||||
|
||||
public String getJavaName() {
|
||||
return javaName;
|
||||
}
|
||||
|
||||
public DbObjectSchema getSchema() {
|
||||
return schema;
|
||||
}
|
||||
|
||||
public DbObjectSchema getConnectedSchema() {
|
||||
if (connectedType == null) return null;
|
||||
return schema.getDbAdmin().findSchemaByClass(connectedType);
|
||||
}
|
||||
|
||||
public void setSchema(DbObjectSchema schema) {
|
||||
this.schema = schema;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public Field getPrimitiveField() {
|
||||
return field;
|
||||
}
|
||||
|
||||
public void setField(Field field) {
|
||||
this.field = field;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return dbName;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.dbName = name;
|
||||
}
|
||||
|
||||
public DbFieldType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(DbFieldType type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public void setPrimaryKey(boolean primaryKey) {
|
||||
this.primaryKey = primaryKey;
|
||||
}
|
||||
|
||||
|
||||
public boolean isPrimaryKey() {
|
||||
return primaryKey;
|
||||
}
|
||||
|
||||
public Class<?> getConnectedType() {
|
||||
return connectedType;
|
||||
}
|
||||
|
||||
public void setConnectedType(Class<?> connectedType) {
|
||||
this.connectedType = connectedType;
|
||||
}
|
||||
|
||||
public boolean isForeignKey() {
|
||||
return connectedType != null;
|
||||
}
|
||||
|
||||
public boolean isNullable() {
|
||||
return nullable;
|
||||
}
|
||||
|
||||
public void setNullable(boolean nullable) {
|
||||
this.nullable = nullable;
|
||||
}
|
||||
|
||||
public boolean isBinary() {
|
||||
return type == DbFieldType.BYTE_ARRAY;
|
||||
}
|
||||
|
||||
public boolean isImage() {
|
||||
return field.getAnnotation(DisplayImage.class) != null;
|
||||
}
|
||||
|
||||
public String getFormat() {
|
||||
return format;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DbField [name=" + dbName + ", javaName=" + javaName + ", type=" + type + ", field=" + field
|
||||
+ ", connectedType=" + connectedType + ", primaryKey=" + primaryKey + ", nullable=" + nullable
|
||||
+ ", schema=" + schema.getClassName() + "]";
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
385
src/main/java/tech/ailef/dbadmin/external/dbmapping/DbFieldType.java
vendored
Normal file
385
src/main/java/tech/ailef/dbadmin/external/dbmapping/DbFieldType.java
vendored
Normal file
@@ -0,0 +1,385 @@
|
||||
package tech.ailef.dbadmin.external.dbmapping;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import jakarta.persistence.ManyToMany;
|
||||
import jakarta.persistence.OneToMany;
|
||||
import jakarta.persistence.OneToOne;
|
||||
import tech.ailef.dbadmin.external.dto.CompareOperator;
|
||||
import tech.ailef.dbadmin.external.exceptions.DbAdminException;
|
||||
|
||||
public enum DbFieldType {
|
||||
INTEGER {
|
||||
@Override
|
||||
public String getHTMLName() {
|
||||
return "number";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object parseValue(Object value) {
|
||||
return Integer.parseInt(value.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
public Class<?> getJavaClass() {
|
||||
return Integer.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CompareOperator> getCompareOperators() {
|
||||
return List.of(CompareOperator.GT, CompareOperator.EQ, CompareOperator.LT);
|
||||
}
|
||||
},
|
||||
DOUBLE {
|
||||
@Override
|
||||
public String getHTMLName() {
|
||||
return "number";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object parseValue(Object value) {
|
||||
return Double.parseDouble(value.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getJavaClass() {
|
||||
return Double.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CompareOperator> getCompareOperators() {
|
||||
return List.of(CompareOperator.GT, CompareOperator.EQ, CompareOperator.LT);
|
||||
}
|
||||
},
|
||||
LONG {
|
||||
@Override
|
||||
public String getHTMLName() {
|
||||
return "number";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object parseValue(Object value) {
|
||||
return Long.parseLong(value.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getJavaClass() {
|
||||
return Long.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CompareOperator> getCompareOperators() {
|
||||
return List.of(CompareOperator.GT, CompareOperator.EQ, CompareOperator.LT);
|
||||
}
|
||||
},
|
||||
FLOAT {
|
||||
@Override
|
||||
public String getHTMLName() {
|
||||
return "number";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object parseValue(Object value) {
|
||||
return Float.parseFloat(value.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getJavaClass() {
|
||||
return Float.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CompareOperator> getCompareOperators() {
|
||||
return List.of(CompareOperator.GT, CompareOperator.EQ, CompareOperator.LT);
|
||||
}
|
||||
},
|
||||
LOCAL_DATE {
|
||||
@Override
|
||||
public String getHTMLName() {
|
||||
return "date";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object parseValue(Object value) {
|
||||
return LocalDate.parse(value.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getJavaClass() {
|
||||
return Float.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CompareOperator> getCompareOperators() {
|
||||
return List.of(CompareOperator.AFTER, CompareOperator.STRING_EQ, CompareOperator.BEFORE);
|
||||
}
|
||||
},
|
||||
LOCAL_DATE_TIME {
|
||||
@Override
|
||||
public String getHTMLName() {
|
||||
return "datetime-local";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object parseValue(Object value) {
|
||||
return LocalDateTime.parse(value.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getJavaClass() {
|
||||
return LocalDateTime.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CompareOperator> getCompareOperators() {
|
||||
return List.of(CompareOperator.AFTER, CompareOperator.STRING_EQ, CompareOperator.BEFORE);
|
||||
}
|
||||
},
|
||||
STRING {
|
||||
@Override
|
||||
public String getHTMLName() {
|
||||
return "text";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object parseValue(Object value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getJavaClass() {
|
||||
return String.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CompareOperator> getCompareOperators() {
|
||||
return List.of(CompareOperator.CONTAINS, CompareOperator.STRING_EQ);
|
||||
}
|
||||
},
|
||||
BOOLEAN {
|
||||
@Override
|
||||
public String getHTMLName() {
|
||||
return "text";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object parseValue(Object value) {
|
||||
return Boolean.parseBoolean(value.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getJavaClass() {
|
||||
return Boolean.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CompareOperator> getCompareOperators() {
|
||||
return List.of(CompareOperator.EQ);
|
||||
}
|
||||
},
|
||||
BIG_DECIMAL {
|
||||
@Override
|
||||
public String getHTMLName() {
|
||||
return "number";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object parseValue(Object value) {
|
||||
return new BigDecimal(value.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getJavaClass() {
|
||||
return BigDecimal.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CompareOperator> getCompareOperators() {
|
||||
return List.of(CompareOperator.GT, CompareOperator.EQ, CompareOperator.LT);
|
||||
}
|
||||
},
|
||||
BYTE_ARRAY {
|
||||
@Override
|
||||
public String getHTMLName() {
|
||||
return "file";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object parseValue(Object value) {
|
||||
try {
|
||||
return ((MultipartFile)value).getBytes();
|
||||
} catch (IOException e) {
|
||||
throw new DbAdminException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getJavaClass() {
|
||||
return byte[].class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CompareOperator> getCompareOperators() {
|
||||
throw new DbAdminException("Binary fields are not comparable");
|
||||
}
|
||||
},
|
||||
ONE_TO_MANY {
|
||||
@Override
|
||||
public String getHTMLName() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object parseValue(Object value) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getJavaClass() {
|
||||
return OneToMany.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRelationship() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "One to Many";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CompareOperator> getCompareOperators() {
|
||||
throw new DbAdminException();
|
||||
}
|
||||
},
|
||||
ONE_TO_ONE {
|
||||
@Override
|
||||
public String getHTMLName() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object parseValue(Object value) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getJavaClass() {
|
||||
return OneToOne.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRelationship() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "One to One";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CompareOperator> getCompareOperators() {
|
||||
throw new DbAdminException();
|
||||
}
|
||||
},
|
||||
MANY_TO_MANY {
|
||||
@Override
|
||||
public String getHTMLName() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object parseValue(Object value) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getJavaClass() {
|
||||
return ManyToMany.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRelationship() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Many to Many";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CompareOperator> getCompareOperators() {
|
||||
throw new DbAdminException();
|
||||
}
|
||||
},
|
||||
COMPUTED {
|
||||
@Override
|
||||
public String getHTMLName() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object parseValue(Object value) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getJavaClass() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CompareOperator> getCompareOperators() {
|
||||
throw new DbAdminException();
|
||||
}
|
||||
};
|
||||
|
||||
public abstract String getHTMLName();
|
||||
|
||||
public abstract Object parseValue(Object value);
|
||||
|
||||
public abstract Class<?> getJavaClass();
|
||||
|
||||
public abstract List<CompareOperator> getCompareOperators();
|
||||
|
||||
public boolean isRelationship() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static DbFieldType fromClass(Class<?> klass) {
|
||||
if (klass == Boolean.class || klass == boolean.class) {
|
||||
return BOOLEAN;
|
||||
} else if (klass == Long.class || klass == long.class) {
|
||||
return LONG;
|
||||
} else if (klass == Integer.class || klass == int.class) {
|
||||
return INTEGER;
|
||||
} else if (klass == String.class) {
|
||||
return STRING;
|
||||
} else if (klass == LocalDate.class) {
|
||||
return LOCAL_DATE;
|
||||
} else if (klass == LocalDateTime.class) {
|
||||
return LOCAL_DATE_TIME;
|
||||
} else if (klass == Float.class || klass == float.class) {
|
||||
return FLOAT;
|
||||
} else if (klass == Double.class || klass == double.class) {
|
||||
return DOUBLE;
|
||||
} else if (klass == BigDecimal.class) {
|
||||
return BIG_DECIMAL;
|
||||
} else if (klass == byte[].class) {
|
||||
return BYTE_ARRAY;
|
||||
} else {
|
||||
throw new DbAdminException("Unsupported field type: " + klass);
|
||||
}
|
||||
}
|
||||
}
|
||||
44
src/main/java/tech/ailef/dbadmin/external/dbmapping/DbFieldValue.java
vendored
Normal file
44
src/main/java/tech/ailef/dbadmin/external/dbmapping/DbFieldValue.java
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
package tech.ailef.dbadmin.external.dbmapping;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
public class DbFieldValue {
|
||||
private Object value;
|
||||
|
||||
private DbField field;
|
||||
|
||||
public DbFieldValue(Object value, DbField field) {
|
||||
this.value = value;
|
||||
this.field = field;
|
||||
}
|
||||
|
||||
public Object getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public String getFormattedValue() {
|
||||
if (value == null) return null;
|
||||
|
||||
if (field.getFormat() == null) {
|
||||
return value.toString();
|
||||
} else {
|
||||
return String.format(field.getFormat(), value);
|
||||
}
|
||||
}
|
||||
|
||||
public DbField getField() {
|
||||
return field;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public String getJavaName() {
|
||||
return field.getPrimitiveField().getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DbFieldValue [value=" + value + ", field=" + field + "]";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
204
src/main/java/tech/ailef/dbadmin/external/dbmapping/DbObject.java
vendored
Normal file
204
src/main/java/tech/ailef/dbadmin/external/dbmapping/DbObject.java
vendored
Normal file
@@ -0,0 +1,204 @@
|
||||
package tech.ailef.dbadmin.external.dbmapping;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import jakarta.persistence.ManyToMany;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.OneToMany;
|
||||
import jakarta.persistence.OneToOne;
|
||||
import tech.ailef.dbadmin.external.annotations.DisplayName;
|
||||
import tech.ailef.dbadmin.external.exceptions.DbAdminException;
|
||||
import tech.ailef.dbadmin.external.misc.Utils;
|
||||
|
||||
public class DbObject {
|
||||
private Object instance;
|
||||
|
||||
private DbObjectSchema schema;
|
||||
|
||||
public DbObject(Object instance, DbObjectSchema schema) {
|
||||
if (instance == null)
|
||||
throw new DbAdminException("Trying to build object with instance == null");
|
||||
|
||||
this.instance = instance;
|
||||
this.schema = schema;
|
||||
}
|
||||
|
||||
public boolean has(DbField field) {
|
||||
return findGetter(field.getJavaName()) != null;
|
||||
}
|
||||
|
||||
public Object getUnderlyingInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<DbObject> getValues(DbField field) {
|
||||
List<Object> values = (List<Object>)get(field.getJavaName()).getValue();
|
||||
return values.stream().map(o -> new DbObject(o, field.getConnectedSchema()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public DbFieldValue get(DbField field) {
|
||||
return get(field.getJavaName());
|
||||
}
|
||||
|
||||
public DbObject traverse(String fieldName) {
|
||||
DbField field = schema.getFieldByName(fieldName);
|
||||
return traverse(field);
|
||||
}
|
||||
|
||||
public DbObject traverse(DbField field) {
|
||||
ManyToOne manyToOne = field.getPrimitiveField().getAnnotation(ManyToOne.class);
|
||||
OneToOne oneToOne = field.getPrimitiveField().getAnnotation(OneToOne.class);
|
||||
if (oneToOne != null || manyToOne != null) {
|
||||
Object linkedObject = get(field.getJavaName()).getValue();
|
||||
if (linkedObject == null) return null;
|
||||
|
||||
DbObject linkedDbObject = new DbObject(linkedObject, field.getConnectedSchema());
|
||||
return linkedDbObject;
|
||||
} else {
|
||||
throw new DbAdminException("Cannot traverse field " + field.getName() + " in class " + schema.getClassName());
|
||||
}
|
||||
}
|
||||
|
||||
public List<DbObject> traverseMany(String fieldName) {
|
||||
DbField field = schema.getFieldByName(fieldName);
|
||||
return traverseMany(field);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<DbObject> traverseMany(DbField field) {
|
||||
ManyToMany manyToMany = field.getPrimitiveField().getAnnotation(ManyToMany.class);
|
||||
OneToMany oneToMany = field.getPrimitiveField().getAnnotation(OneToMany.class);
|
||||
if (manyToMany != null || oneToMany != null) {
|
||||
List<Object> linkedObjects = (List<Object>)get(field.getJavaName()).getValue();
|
||||
return linkedObjects.stream().map(o -> new DbObject(o, field.getConnectedSchema()))
|
||||
.collect(Collectors.toList());
|
||||
} else {
|
||||
throw new DbAdminException("Cannot traverse field " + field.getName() + " in class " + schema.getClassName());
|
||||
}
|
||||
}
|
||||
|
||||
public DbFieldValue get(String name) {
|
||||
Method getter = findGetter(name);
|
||||
|
||||
if (getter == null)
|
||||
throw new DbAdminException("Unable to find getter method for field `"
|
||||
+ name + "` in class " + instance.getClass());
|
||||
|
||||
try {
|
||||
Object result = getter.invoke(instance);
|
||||
return new DbFieldValue(result, schema.getFieldByJavaName(name));
|
||||
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
|
||||
throw new DbAdminException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public Object getPrimaryKeyValue() {
|
||||
DbField primaryKeyField = schema.getPrimaryKey();
|
||||
Method getter = findGetter(primaryKeyField.getJavaName());
|
||||
|
||||
if (getter == null)
|
||||
throw new DbAdminException("Unable to find getter method for field `"
|
||||
+ primaryKeyField.getJavaName() + "` in class " + instance.getClass());
|
||||
|
||||
try {
|
||||
Object result = getter.invoke(instance);
|
||||
return result;
|
||||
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
|
||||
throw new DbAdminException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
Method[] methods = instance.getClass().getMethods();
|
||||
|
||||
Optional<Method> displayNameMethod =
|
||||
Arrays.stream(methods)
|
||||
.filter(m -> m.getAnnotation(DisplayName.class) != null)
|
||||
.findFirst();
|
||||
|
||||
if (displayNameMethod.isPresent()) {
|
||||
try {
|
||||
Object displayName = displayNameMethod.get().invoke(instance);
|
||||
if (displayName == null) return null;
|
||||
else return displayName.toString();
|
||||
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
|
||||
throw new DbAdminException(e);
|
||||
}
|
||||
} else {
|
||||
return getPrimaryKeyValue().toString();
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> getComputedColumns() {
|
||||
return schema.getComputedColumnNames();
|
||||
}
|
||||
|
||||
public Object compute(String column) {
|
||||
Method method = schema.getComputedColumn(column);
|
||||
|
||||
if (method == null)
|
||||
throw new DbAdminException("Unable to find mapped method for @ComputedColumn " + column);
|
||||
|
||||
try {
|
||||
return method.invoke(instance);
|
||||
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
|
||||
throw new DbAdminException("Error while calling @ComputedColumn " + column
|
||||
+ " on class " + schema.getClassName());
|
||||
}
|
||||
}
|
||||
|
||||
public void set(String fieldName, Object value) {
|
||||
Method setter = findSetter(fieldName);
|
||||
|
||||
if (setter == null) {
|
||||
throw new DbAdminException("Unable to find setter method for " + fieldName + " in " + schema.getClassName());
|
||||
}
|
||||
|
||||
try {
|
||||
setter.invoke(instance, value);
|
||||
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private Method findSetter(String fieldName) {
|
||||
fieldName = Utils.snakeToCamel(fieldName);
|
||||
String capitalize = Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);
|
||||
Method[] methods = instance.getClass().getDeclaredMethods();
|
||||
|
||||
for (Method m : methods) {
|
||||
if (m.getName().equals("set" + capitalize))
|
||||
return m;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Method findGetter(String fieldName) {
|
||||
String capitalize = Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);
|
||||
Method[] methods = instance.getClass().getDeclaredMethods();
|
||||
|
||||
DbField dbField = schema.getFieldByJavaName(fieldName);
|
||||
if (dbField == null) return null;
|
||||
|
||||
String prefix = "get";
|
||||
if (dbField.getType() == DbFieldType.BOOLEAN) {
|
||||
prefix = "is";
|
||||
}
|
||||
|
||||
for (Method m : methods) {
|
||||
if (m.getName().equals(prefix + capitalize))
|
||||
return m;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
194
src/main/java/tech/ailef/dbadmin/external/dbmapping/DbObjectSchema.java
vendored
Normal file
194
src/main/java/tech/ailef/dbadmin/external/dbmapping/DbObjectSchema.java
vendored
Normal file
@@ -0,0 +1,194 @@
|
||||
package tech.ailef.dbadmin.external.dbmapping;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
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.external.DbAdmin;
|
||||
import tech.ailef.dbadmin.external.annotations.ComputedColumn;
|
||||
import tech.ailef.dbadmin.external.annotations.Filterable;
|
||||
import tech.ailef.dbadmin.external.exceptions.DbAdminException;
|
||||
import tech.ailef.dbadmin.external.misc.Utils;
|
||||
|
||||
public class DbObjectSchema {
|
||||
/**
|
||||
* All the fields in this table. The fields include all the
|
||||
* columns present in the table plus relationship fields.
|
||||
*/
|
||||
@JsonIgnore
|
||||
private List<DbField> fields = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* The methods designated as computed columns in the `@Entity` class.
|
||||
*/
|
||||
@JsonIgnore
|
||||
private Map<String, Method> computedColumns = new HashMap<>();
|
||||
|
||||
/**
|
||||
* A JPA repository to operate on the database
|
||||
*/
|
||||
private AdvancedJpaRepository jpaRepository;
|
||||
|
||||
private DbAdmin dbAdmin;
|
||||
|
||||
/**
|
||||
* The corresponding `@Entity` class that this schema describes
|
||||
*/
|
||||
@JsonIgnore
|
||||
private Class<?> entityClass;
|
||||
|
||||
/**
|
||||
* The name of this table on the database
|
||||
*/
|
||||
private String tableName;
|
||||
|
||||
public DbObjectSchema(Class<?> klass, DbAdmin dbAdmin) {
|
||||
this.dbAdmin = dbAdmin;
|
||||
this.entityClass = klass;
|
||||
|
||||
Table tableAnnotation = klass.getAnnotation(Table.class);
|
||||
|
||||
String tableName = Utils.camelToSnake(getJavaClass().getSimpleName());
|
||||
if (tableAnnotation != null && tableAnnotation.name() != null
|
||||
&& !tableAnnotation.name().isBlank()) {
|
||||
tableName = tableAnnotation.name();
|
||||
}
|
||||
|
||||
this.tableName = tableName;
|
||||
|
||||
List<Method> methods = Arrays.stream(entityClass.getMethods())
|
||||
.filter(m -> m.getAnnotation(ComputedColumn.class) != null)
|
||||
.collect(Collectors.toList());
|
||||
for (Method m : methods) {
|
||||
if (m.getParameterCount() > 0)
|
||||
throw new DbAdminException("@ComputedColumn can only be applied on no-args methods");
|
||||
|
||||
String name = m.getAnnotation(ComputedColumn.class).name();
|
||||
if (name.isBlank())
|
||||
name = Utils.camelToSnake(m.getName());
|
||||
|
||||
computedColumns.put(name, m);
|
||||
}
|
||||
}
|
||||
|
||||
public DbAdmin getDbAdmin() {
|
||||
return dbAdmin;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public Class<?> getJavaClass() {
|
||||
return entityClass;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public String getClassName() {
|
||||
return entityClass.getName();
|
||||
}
|
||||
|
||||
public List<DbField> getFields() {
|
||||
return Collections.unmodifiableList(fields);
|
||||
}
|
||||
|
||||
public DbField getFieldByJavaName(String name) {
|
||||
return fields.stream().filter(f -> f.getJavaName().equals(name)).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
public DbField getFieldByName(String name) {
|
||||
return fields.stream().filter(f -> f.getName().equals(name)).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
public void addField(DbField f) {
|
||||
fields.add(f);
|
||||
}
|
||||
|
||||
public AdvancedJpaRepository getJpaRepository() {
|
||||
return jpaRepository;
|
||||
}
|
||||
|
||||
public void setJpaRepository(AdvancedJpaRepository jpaRepository) {
|
||||
this.jpaRepository = jpaRepository;
|
||||
}
|
||||
|
||||
public String getTableName() {
|
||||
return tableName;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public List<DbField> getSortedFields() {
|
||||
return getFields().stream()
|
||||
.filter(f -> {
|
||||
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())
|
||||
return -1;
|
||||
if (b.isPrimaryKey() && !a.isPrimaryKey())
|
||||
return 1;
|
||||
return a.getName().compareTo(b.getName());
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public List<DbField> getRelationshipFields() {
|
||||
List<DbField> res = getFields().stream().filter(f -> {
|
||||
return f.getPrimitiveField().getAnnotation(OneToMany.class) != null
|
||||
|| f.getPrimitiveField().getAnnotation(ManyToMany.class) != null;
|
||||
}).collect(Collectors.toList());
|
||||
return res;
|
||||
}
|
||||
|
||||
public List<DbField> getManyToManyOwnedFields() {
|
||||
List<DbField> res = getFields().stream().filter(f -> {
|
||||
ManyToMany anno = f.getPrimitiveField().getAnnotation(ManyToMany.class);
|
||||
return anno != null && anno.mappedBy().isBlank();
|
||||
}).collect(Collectors.toList());
|
||||
return res;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public DbField getPrimaryKey() {
|
||||
Optional<DbField> pk = fields.stream().filter(f -> f.isPrimaryKey()).findFirst();
|
||||
if (pk.isPresent())
|
||||
return pk.get();
|
||||
else
|
||||
throw new RuntimeException("No primary key defined on " + entityClass.getName() + " (table `" + tableName + "`)");
|
||||
}
|
||||
|
||||
public List<String> getComputedColumnNames() {
|
||||
return computedColumns.keySet().stream().sorted().toList();
|
||||
}
|
||||
|
||||
public Method getComputedColumn(String name) {
|
||||
return computedColumns.get(name);
|
||||
}
|
||||
|
||||
public List<DbField> getFilterableFields() {
|
||||
return getSortedFields().stream().filter(f -> {
|
||||
return !f.isBinary() && !f.isPrimaryKey()
|
||||
&& f.getPrimitiveField().getAnnotation(Filterable.class) != null;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DbObjectSchema [fields=" + fields + ", className=" + entityClass.getName() + "]";
|
||||
}
|
||||
|
||||
}
|
||||
34
src/main/java/tech/ailef/dbadmin/external/dto/AutocompleteSearchResult.java
vendored
Normal file
34
src/main/java/tech/ailef/dbadmin/external/dto/AutocompleteSearchResult.java
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
package tech.ailef.dbadmin.external.dto;
|
||||
|
||||
import tech.ailef.dbadmin.external.dbmapping.DbObject;
|
||||
|
||||
public class AutocompleteSearchResult {
|
||||
private Object id;
|
||||
|
||||
private String value;
|
||||
|
||||
public AutocompleteSearchResult() {
|
||||
}
|
||||
|
||||
public AutocompleteSearchResult(DbObject o) {
|
||||
this.id = o.getPrimaryKeyValue();
|
||||
this.value = o.getDisplayName();
|
||||
}
|
||||
|
||||
public Object getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Object id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public void setValue(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
}
|
||||
52
src/main/java/tech/ailef/dbadmin/external/dto/CompareOperator.java
vendored
Normal file
52
src/main/java/tech/ailef/dbadmin/external/dto/CompareOperator.java
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
package tech.ailef.dbadmin.external.dto;
|
||||
|
||||
public enum CompareOperator {
|
||||
GT {
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return "Greater than";
|
||||
}
|
||||
},
|
||||
LT {
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return "Less than";
|
||||
}
|
||||
},
|
||||
EQ {
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return "Equals";
|
||||
}
|
||||
},
|
||||
STRING_EQ {
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return "Equals";
|
||||
}
|
||||
},
|
||||
BEFORE {
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return "Before";
|
||||
}
|
||||
},
|
||||
AFTER {
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return "After";
|
||||
}
|
||||
},
|
||||
CONTAINS {
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return "Contains";
|
||||
}
|
||||
};
|
||||
|
||||
public abstract String getDisplayName();
|
||||
|
||||
public String toString() {
|
||||
return this.name().toLowerCase();
|
||||
}
|
||||
}
|
||||
98
src/main/java/tech/ailef/dbadmin/external/dto/LogsSearchRequest.java
vendored
Normal file
98
src/main/java/tech/ailef/dbadmin/external/dto/LogsSearchRequest.java
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
package tech.ailef.dbadmin.external.dto;
|
||||
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
|
||||
public class LogsSearchRequest {
|
||||
private String table;
|
||||
|
||||
private String actionType;
|
||||
|
||||
private String itemId;
|
||||
|
||||
private int page;
|
||||
|
||||
private int pageSize;
|
||||
|
||||
private String sortKey;
|
||||
|
||||
private String sortOrder;
|
||||
|
||||
public String getTable() {
|
||||
return table == null || table.isBlank() || table.equalsIgnoreCase("Any") ? null : table;
|
||||
}
|
||||
|
||||
public void setTable(String table) {
|
||||
this.table = table;
|
||||
}
|
||||
|
||||
public String getActionType() {
|
||||
return actionType == null || actionType.isBlank() || actionType.equalsIgnoreCase("Any") ? null : actionType;
|
||||
}
|
||||
|
||||
public void setActionType(String actionType) {
|
||||
this.actionType = actionType;
|
||||
}
|
||||
|
||||
public String getItemId() {
|
||||
return itemId == null || itemId.isBlank() ? null : itemId;
|
||||
}
|
||||
|
||||
public void setItemId(String itemId) {
|
||||
this.itemId = itemId;
|
||||
}
|
||||
|
||||
public int getPage() {
|
||||
return page;
|
||||
}
|
||||
|
||||
public void setPage(int page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
public int getPageSize() {
|
||||
return pageSize;
|
||||
}
|
||||
|
||||
public void setPageSize(int pageSize) {
|
||||
this.pageSize = pageSize;
|
||||
}
|
||||
|
||||
public String getSortKey() {
|
||||
return sortKey;
|
||||
}
|
||||
|
||||
public void setSortKey(String sortKey) {
|
||||
this.sortKey = sortKey;
|
||||
}
|
||||
|
||||
public String getSortOrder() {
|
||||
return sortOrder;
|
||||
}
|
||||
|
||||
public void setSortOrder(String sortOrder) {
|
||||
this.sortOrder = sortOrder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "LogsSearchRequest [table=" + table + ", actionType=" + actionType + ", itemId=" + itemId + ", page="
|
||||
+ page + ", pageSize=" + pageSize + ", sortKey=" + sortKey + ", sortOrder=" + sortOrder + "]";
|
||||
}
|
||||
|
||||
public PageRequest toPageRequest() {
|
||||
int actualPage = page - 1 < 0 ? 0 : page - 1;
|
||||
int actualPageSize = pageSize <= 0 ? 50 : pageSize;
|
||||
if (sortKey == null)
|
||||
return PageRequest.of(actualPage, actualPageSize);
|
||||
|
||||
if (sortOrder == null) sortOrder = "ASC";
|
||||
|
||||
if (sortOrder.equals("DESC")) {
|
||||
return PageRequest.of(actualPage, actualPageSize, Sort.by(sortKey).descending());
|
||||
} else {
|
||||
return PageRequest.of(actualPage, actualPageSize, Sort.by(sortKey).ascending());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
32
src/main/java/tech/ailef/dbadmin/external/dto/PaginatedResult.java
vendored
Normal file
32
src/main/java/tech/ailef/dbadmin/external/dto/PaginatedResult.java
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
package tech.ailef.dbadmin.external.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class PaginatedResult<T> {
|
||||
private PaginationInfo pagination;
|
||||
|
||||
private List<T> results;
|
||||
|
||||
public PaginatedResult(PaginationInfo pagination, List<T> page) {
|
||||
this.pagination = pagination;
|
||||
this.results = page;
|
||||
}
|
||||
|
||||
public PaginationInfo getPagination() {
|
||||
return pagination;
|
||||
}
|
||||
|
||||
public List<T> getResults() {
|
||||
return results;
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return results.isEmpty();
|
||||
}
|
||||
|
||||
public int getNumberOfResults() {
|
||||
return getResults().size();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
131
src/main/java/tech/ailef/dbadmin/external/dto/PaginationInfo.java
vendored
Normal file
131
src/main/java/tech/ailef/dbadmin/external/dto/PaginationInfo.java
vendored
Normal file
@@ -0,0 +1,131 @@
|
||||
package tech.ailef.dbadmin.external.dto;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
import tech.ailef.dbadmin.external.misc.Utils;
|
||||
|
||||
/**
|
||||
* Attached as output to requests that have a paginated response,
|
||||
* holds information about the current pagination.
|
||||
*/
|
||||
public class PaginationInfo {
|
||||
/**
|
||||
* How many previous and next pages to generate, used in the front-end navigation
|
||||
*/
|
||||
private static final int PAGE_RANGE = 3;
|
||||
|
||||
/**
|
||||
* The current page of results
|
||||
*/
|
||||
private int currentPage;
|
||||
|
||||
/**
|
||||
* The last page for which there are results
|
||||
*/
|
||||
private int maxPage;
|
||||
|
||||
/**
|
||||
* The current number of elements per page
|
||||
*/
|
||||
private int pageSize;
|
||||
|
||||
// TODO: Check if used
|
||||
private long maxElement;
|
||||
|
||||
private Set<QueryFilter> queryFilters;
|
||||
|
||||
private String query;
|
||||
|
||||
public PaginationInfo(int currentPage, int maxPage, int pageSize, long maxElement, String query, Set<QueryFilter> queryFilters) {
|
||||
this.currentPage = currentPage;
|
||||
this.maxPage = maxPage;
|
||||
this.pageSize = pageSize;
|
||||
this.query = query;
|
||||
this.maxElement = maxElement;
|
||||
this.queryFilters = queryFilters;
|
||||
}
|
||||
|
||||
public int getCurrentPage() {
|
||||
return currentPage;
|
||||
}
|
||||
|
||||
public void setCurrentPage(int currentPage) {
|
||||
this.currentPage = currentPage;
|
||||
}
|
||||
|
||||
public int getMaxPage() {
|
||||
return maxPage;
|
||||
}
|
||||
|
||||
public void setMaxPage(int maxPage) {
|
||||
this.maxPage = maxPage;
|
||||
}
|
||||
|
||||
public int getPageSize() {
|
||||
return pageSize;
|
||||
}
|
||||
|
||||
public void setPageSize(int pageSize) {
|
||||
this.pageSize = pageSize;
|
||||
}
|
||||
|
||||
public long getMaxElement() {
|
||||
return maxElement;
|
||||
}
|
||||
|
||||
public String getSortedPageLink(String sortKey, String sortOrder) {
|
||||
MultiValueMap<String, String> params = Utils.computeParams(queryFilters);
|
||||
|
||||
if (query != null) {
|
||||
params.put("query", new ArrayList<>());
|
||||
params.get("query").add(query);
|
||||
}
|
||||
|
||||
params.add("pageSize", "" + pageSize);
|
||||
params.add("page", "" + currentPage);
|
||||
params.add("sortKey", sortKey);
|
||||
params.add("sortOrder", sortOrder);
|
||||
|
||||
return Utils.getQueryString(params);
|
||||
}
|
||||
|
||||
public String getLink(int page) {
|
||||
MultiValueMap<String, String> params = Utils.computeParams(queryFilters);
|
||||
|
||||
if (query != null) {
|
||||
params.put("query", new ArrayList<>());
|
||||
params.get("query").add(query);
|
||||
}
|
||||
|
||||
params.add("pageSize", "" + pageSize);
|
||||
params.add("page", "" + page);
|
||||
|
||||
return Utils.getQueryString(params);
|
||||
}
|
||||
|
||||
public List<Integer> getBeforePages() {
|
||||
return IntStream.range(Math.max(currentPage - PAGE_RANGE, 1), currentPage).boxed().collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public List<Integer> getAfterPages() {
|
||||
return IntStream.range(currentPage + 1, Math.min(currentPage + PAGE_RANGE, maxPage + 1)).boxed().collect(Collectors.toList());
|
||||
}
|
||||
//
|
||||
// public String getSortKey() {
|
||||
// return sortKey;
|
||||
// }
|
||||
//
|
||||
// public String getSortOrder() {
|
||||
// return sortOrder;
|
||||
// }
|
||||
|
||||
public boolean isLastPage() {
|
||||
return currentPage == maxPage;
|
||||
}
|
||||
}
|
||||
57
src/main/java/tech/ailef/dbadmin/external/dto/QueryFilter.java
vendored
Normal file
57
src/main/java/tech/ailef/dbadmin/external/dto/QueryFilter.java
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
package tech.ailef.dbadmin.external.dto;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class QueryFilter {
|
||||
private String field;
|
||||
|
||||
private CompareOperator op;
|
||||
|
||||
private String value;
|
||||
|
||||
public QueryFilter(String field, CompareOperator op, String value) {
|
||||
this.field = field;
|
||||
this.op = op;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getField() {
|
||||
return field;
|
||||
}
|
||||
|
||||
public CompareOperator getOp() {
|
||||
return op;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(field, op, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String displayValue = value;
|
||||
if (value.length() > 10) {
|
||||
displayValue = value.substring(0, 4) + "..." + value.substring(value.length() - 4);
|
||||
}
|
||||
return "'" + field + "' " + op.getDisplayName() + " '" + displayValue + "'";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
QueryFilter other = (QueryFilter) obj;
|
||||
return Objects.equals(field, other.field) && Objects.equals(op, other.op) && Objects.equals(value, other.value);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
16
src/main/java/tech/ailef/dbadmin/external/exceptions/DbAdminException.java
vendored
Normal file
16
src/main/java/tech/ailef/dbadmin/external/exceptions/DbAdminException.java
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
package tech.ailef.dbadmin.external.exceptions;
|
||||
|
||||
public class DbAdminException extends RuntimeException {
|
||||
private static final long serialVersionUID = 8120227031645804467L;
|
||||
|
||||
public DbAdminException() {
|
||||
}
|
||||
|
||||
public DbAdminException(Throwable e) {
|
||||
super(e);
|
||||
}
|
||||
|
||||
public DbAdminException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
19
src/main/java/tech/ailef/dbadmin/external/exceptions/InvalidPageException.java
vendored
Normal file
19
src/main/java/tech/ailef/dbadmin/external/exceptions/InvalidPageException.java
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
package tech.ailef.dbadmin.external.exceptions;
|
||||
|
||||
/**
|
||||
* Thrown during the computation of pagination if the requested
|
||||
* page number is not valid within the current request (e.g. it is greater
|
||||
* than the maximum available page). Used internally to redirect the
|
||||
* user to a default page.
|
||||
*/
|
||||
public class InvalidPageException extends DbAdminException {
|
||||
private static final long serialVersionUID = -8891734807568233099L;
|
||||
|
||||
public InvalidPageException() {
|
||||
}
|
||||
|
||||
public InvalidPageException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
}
|
||||
103
src/main/java/tech/ailef/dbadmin/external/misc/Utils.java
vendored
Normal file
103
src/main/java/tech/ailef/dbadmin/external/misc/Utils.java
vendored
Normal file
@@ -0,0 +1,103 @@
|
||||
package tech.ailef.dbadmin.external.misc;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
import tech.ailef.dbadmin.external.dto.CompareOperator;
|
||||
import tech.ailef.dbadmin.external.dto.QueryFilter;
|
||||
import tech.ailef.dbadmin.external.exceptions.DbAdminException;
|
||||
|
||||
public interface Utils {
|
||||
public static String camelToSnake(String v) {
|
||||
if (Character.isUpperCase(v.charAt(0))) {
|
||||
v = Character.toLowerCase(v.charAt(0)) + v.substring(1);
|
||||
}
|
||||
|
||||
return v.replaceAll("([A-Z][a-z])", "_$1").toLowerCase();
|
||||
|
||||
}
|
||||
|
||||
public static MultiValueMap<String, String> computeParams(Set<QueryFilter> filters) {
|
||||
MultiValueMap<String, String> r = new LinkedMultiValueMap<>();
|
||||
if (filters == null)
|
||||
return r;
|
||||
|
||||
r.put("filter_field", new ArrayList<>());
|
||||
r.put("filter_op", new ArrayList<>());
|
||||
r.put("filter_value", new ArrayList<>());
|
||||
|
||||
for (QueryFilter filter : filters) {
|
||||
r.get("filter_field").add(filter.getField());
|
||||
r.get("filter_op").add(filter.getOp().toString());
|
||||
r.get("filter_value").add(filter.getValue());
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
public static Set<QueryFilter> computeFilters(MultiValueMap<String, String> params) {
|
||||
if (params == null)
|
||||
return new HashSet<>();
|
||||
|
||||
List<String> ops = params.get("filter_op");
|
||||
List<String> fields = params.get("filter_field");
|
||||
List<String> values = params.get("filter_value");
|
||||
|
||||
if (ops == null || fields == null || values == null)
|
||||
return new HashSet<>();
|
||||
|
||||
if (ops.size() != fields.size() || fields.size() != values.size()
|
||||
|| ops.size() != values.size()) {
|
||||
throw new DbAdminException("Filtering parameters must have the same size");
|
||||
}
|
||||
|
||||
Set<QueryFilter> filters = new HashSet<>();
|
||||
for (int i = 0; i < ops.size(); i++) {
|
||||
String op = ops.get(i);
|
||||
String field = fields.get(i);
|
||||
String value = values.get(i);
|
||||
|
||||
QueryFilter queryFilter = new QueryFilter(field, CompareOperator.valueOf(op.toUpperCase()), value);
|
||||
filters.add(queryFilter);
|
||||
}
|
||||
|
||||
return filters;
|
||||
|
||||
}
|
||||
|
||||
|
||||
public static String getQueryString(MultiValueMap<String, String> params) {
|
||||
Set<String> currentParams = params.keySet();
|
||||
List<String> paramValues = new ArrayList<>();
|
||||
for (String param : currentParams) {
|
||||
for (String v : params.get(param)) {
|
||||
paramValues.add(param + "=" + v.trim());
|
||||
}
|
||||
}
|
||||
|
||||
if (paramValues.isEmpty()) return "";
|
||||
return "?" + String.join("&", paramValues);
|
||||
}
|
||||
|
||||
public static String snakeToCamel(String text) {
|
||||
boolean shouldConvertNextCharToLower = true;
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
char currentChar = text.charAt(i);
|
||||
if (currentChar == '_') {
|
||||
shouldConvertNextCharToLower = false;
|
||||
} else if (shouldConvertNextCharToLower) {
|
||||
builder.append(Character.toLowerCase(currentChar));
|
||||
} else {
|
||||
builder.append(Character.toUpperCase(currentChar));
|
||||
shouldConvertNextCharToLower = true;
|
||||
}
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user