diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..e5b3b67 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,26 @@ +name: Publish package to the Maven Central Repository +on: + push: + tags: + - v* + pull_request: + branches: [ main ] +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Java + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'adopt' + - name: Publish package + env: + JRELEASER_NEXUS2_USERNAME: ${{ secrets.JRELEASER_NEXUS2_USERNAME }} + JRELEASER_NEXUS2_PASSWORD: ${{ secrets.JRELEASER_NEXUS2_PASSWORD }} + JRELEASER_GPG_PASSPHRASE: ${{ secrets.JRELEASER_GPG_PASSPHRASE }} + JRELEASER_GPG_SECRET_KEY: ${{ secrets.JRELEASER_GPG_SECRET_KEY }} + JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.JRELEASER_GPG_PUBLIC_KEY }} + JRELEASER_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: ./mvnw -Prelease -DskipTests deploy jreleaser:deploy -DaltDeploymentRepository=local::file:./target/staging-deploy diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..cb28b0e Binary files /dev/null and b/.mvn/wrapper/maven-wrapper.jar differ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2e76e18 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/README.md b/README.md index 71424c2..f015492 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,15 @@ If you find a problem or a bug, please report it as issue. When doing so, includ ## Changelog + * provide the code for the involved `@Entity` classes, if possible + * provide the full stack trace of the error + * specify if you are using any particular configuration either in your `application.properties` or through annotations + +**0.1.2** +- Better handling of large text fields (shown as `textarea`) +- Added `CATEGORICAL` option to `Filterable` +- Several bug fixes + **0.1.0** - Implemented action logs - Implemented user settings diff --git a/mvnw b/mvnw new file mode 100755 index 0000000..66df285 --- /dev/null +++ b/mvnw @@ -0,0 +1,308 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.2.0 +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "$(uname)" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME + else + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=$(java-config --jre-home) + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then + if $darwin ; then + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" + else + javaExecutable="$(readlink -f "\"$javaExecutable\"")" + fi + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=$(cd "$wdir/.." || exit 1; pwd) + fi + # end of workaround + done + printf '%s' "$(cd "$basedir" || exit 1; pwd)" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" + fi +} + +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +# shellcheck disable=SC2086 # safe args +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/pom.xml b/pom.xml index 51b7fd1..6dae032 100644 --- a/pom.xml +++ b/pom.xml @@ -13,10 +13,78 @@ spring-boot-db-admin 0.1.2 spring-boot-db-admin - Srping Boot DB Admin Dashboard + Srping Boot Database Admin is an auto-generated CRUD admin panel for Spring Boot apps 17 + + + GPL-v3.0 + http://www.gnu.org/licenses/gpl-3.0.txt + + + + + + release + + + + org.jreleaser + jreleaser-maven-plugin + 1.3.1 + + + + ALWAYS + true + + + + + + ALWAYS + https://s01.oss.sonatype.org/service/local + true + true + target/staging-deploy + + + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.4.1 + + + attach-javadoc + + jar + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.2.1 + + + attach-source + + jar + + + + + + + + @@ -25,12 +93,12 @@ 1.3 - - - - - - + + + + + + @@ -88,13 +156,13 @@ - - - - - - - - + + + + + + + + diff --git a/src/main/java/tech/ailef/dbadmin/external/DbAdmin.java b/src/main/java/tech/ailef/dbadmin/external/DbAdmin.java index bbb888e..b3eae7a 100644 --- a/src/main/java/tech/ailef/dbadmin/external/DbAdmin.java +++ b/src/main/java/tech/ailef/dbadmin/external/DbAdmin.java @@ -53,6 +53,12 @@ public class DbAdmin { private String modelsPackage; + /** + * Builds the DbAdmin instance by scanning the `@Entity` beans and loading + * the schemas. + * @param entityManager the entity manager + * @param properties the configuration properties + */ public DbAdmin(@Autowired EntityManager entityManager, @Autowired DbAdminProperties properties) { this.modelsPackage = properties.getModelsPackage(); this.entityManager = entityManager; @@ -71,7 +77,7 @@ public class DbAdmin { /** * Returns all the loaded schemas (i.e. entity classes) - * @return + * @return the list of loaded schemas from the `@Entity` classes */ public List getSchemas() { return Collections.unmodifiableList(schemas); @@ -80,7 +86,7 @@ public class DbAdmin { /** * Finds a schema by its full class name * @param className qualified class name - * @return + * @return the schema with this class name * @throws DbAdminException if corresponding schema not found */ public DbObjectSchema findSchemaByClassName(String className) { @@ -92,7 +98,7 @@ public class DbAdmin { /** * Finds a schema by its table name * @param tableName the table name on the database - * @return + * @return the schema with this table name * @throws DbAdminException if corresponding schema not found */ public DbObjectSchema findSchemaByTableName(String tableName) { @@ -102,9 +108,9 @@ public class DbAdmin { } /** - * Finds a schema by its class - * @param klass - * @return + * Finds a schema by its class object + * @param the `@Entity` class you want to find the schema for + * @return the schema for the `@Entity` class * @throws DbAdminException if corresponding schema not found */ public DbObjectSchema findSchemaByClass(Class klass) { @@ -118,7 +124,7 @@ public class DbAdmin { * * If any field is not mappable, the method will throw an exception. * @param bd - * @return + * @return a schema derived from the `@Entity` class */ private DbObjectSchema processBeanDefinition(BeanDefinition bd) { String fullClassName = bd.getBeanClassName(); diff --git a/src/main/java/tech/ailef/dbadmin/external/DbAdminAutoConfiguration.java b/src/main/java/tech/ailef/dbadmin/external/DbAdminAutoConfiguration.java index 7f11371..8899a20 100644 --- a/src/main/java/tech/ailef/dbadmin/external/DbAdminAutoConfiguration.java +++ b/src/main/java/tech/ailef/dbadmin/external/DbAdminAutoConfiguration.java @@ -21,6 +21,10 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; import tech.ailef.dbadmin.internal.InternalDbAdminConfiguration; +/** + * The configuration class that adds and configures the "internal" data source. + * + */ @ConditionalOnProperty(name = "dbadmin.enabled", matchIfMissing = true) @ComponentScan @EnableConfigurationProperties(DbAdminProperties.class) @@ -56,7 +60,7 @@ public class DbAdminAutoConfiguration { LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean(); factoryBean.setDataSource(internalDataSource()); factoryBean.setPersistenceUnitName("internal"); - factoryBean.setPackagesToScan("tech.ailef.dbadmin.internal.model"); // , "tech.ailef.dbadmin.repository"); + factoryBean.setPackagesToScan("tech.ailef.dbadmin.internal.model"); factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter()); Properties properties = new Properties(); properties.setProperty("hibernate.dialect", "org.hibernate.dialect.H2Dialect"); diff --git a/src/main/java/tech/ailef/dbadmin/external/DbAdminProperties.java b/src/main/java/tech/ailef/dbadmin/external/DbAdminProperties.java index c2e3544..5dd458b 100644 --- a/src/main/java/tech/ailef/dbadmin/external/DbAdminProperties.java +++ b/src/main/java/tech/ailef/dbadmin/external/DbAdminProperties.java @@ -26,6 +26,9 @@ public class DbAdminProperties { */ private String modelsPackage; + /** + * Set to true when running the tests to configure the "internal" data source as in memory + */ private boolean testMode = false; public boolean isEnabled() { diff --git a/src/main/java/tech/ailef/dbadmin/external/annotations/DisplayFormat.java b/src/main/java/tech/ailef/dbadmin/external/annotations/DisplayFormat.java index 7e7641e..7e02b04 100644 --- a/src/main/java/tech/ailef/dbadmin/external/annotations/DisplayFormat.java +++ b/src/main/java/tech/ailef/dbadmin/external/annotations/DisplayFormat.java @@ -13,5 +13,9 @@ import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface DisplayFormat { + /** + * The format to apply to the field's value + * @return + */ public String format() default ""; } \ No newline at end of file diff --git a/src/main/java/tech/ailef/dbadmin/external/annotations/Filterable.java b/src/main/java/tech/ailef/dbadmin/external/annotations/Filterable.java index cbcf71e..4d90bf0 100644 --- a/src/main/java/tech/ailef/dbadmin/external/annotations/Filterable.java +++ b/src/main/java/tech/ailef/dbadmin/external/annotations/Filterable.java @@ -16,5 +16,9 @@ import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface Filterable { + /** + * The type of filter (DEFAULT or CATEGORICAL) + * @return + */ public FilterableType type() default FilterableType.DEFAULT; } \ No newline at end of file diff --git a/src/main/java/tech/ailef/dbadmin/external/annotations/FilterableType.java b/src/main/java/tech/ailef/dbadmin/external/annotations/FilterableType.java index 79cf09b..d38bc63 100644 --- a/src/main/java/tech/ailef/dbadmin/external/annotations/FilterableType.java +++ b/src/main/java/tech/ailef/dbadmin/external/annotations/FilterableType.java @@ -1,5 +1,24 @@ package tech.ailef.dbadmin.external.annotations; +/** + * Type of filters that can be used in the faceted search. + * + */ public enum FilterableType { - DEFAULT, CATEGORICAL; + /** + * The default filter provides a list of standard operators + * customized to the field type (e.g. greater than/less than/equals for numbers, + * after/before/equals for dates, contains/equals for strings, etc...), with, + * if applicable, an autocomplete form if the field references a foreign key. + */ + DEFAULT, + /** + * The categorical filter provides the full list of possible values + * for the field, rendered as a list of clickable items (that will + * filter for equality). This provides a better UX if the field can take + * a limited number of values and it's more convenient to have them all + * on screen rather than typing them. + */ + CATEGORICAL; + } diff --git a/src/main/java/tech/ailef/dbadmin/external/controller/DownloadController.java b/src/main/java/tech/ailef/dbadmin/external/controller/DownloadController.java index 36c8863..f46edce 100644 --- a/src/main/java/tech/ailef/dbadmin/external/controller/DownloadController.java +++ b/src/main/java/tech/ailef/dbadmin/external/controller/DownloadController.java @@ -37,6 +37,13 @@ public class DownloadController { private DbAdmin dbAdmin; + /** + * Serve a binary field as an image + * @param className + * @param fieldName + * @param id + * @return + */ @GetMapping(value="/{className}/{fieldName}/{id}/image", produces = MediaType.IMAGE_JPEG_VALUE) @ResponseBody public ResponseEntity serveImage(@PathVariable String className, @@ -58,6 +65,16 @@ public class DownloadController { } + /** + * Serve a binary field as a file. This tries to detect the file type using Tika + * in order to serve the file with a plausible extension, since we don't have + * any meta-data about what was originally uploaded and it is not feasible to + * store it (it could be modified on another end and we wouldn't be aware of it). + * @param className + * @param fieldName + * @param id + * @return + */ @GetMapping("/{className}/{fieldName}/{id}") @ResponseBody public ResponseEntity serveFile(@PathVariable String className, diff --git a/src/main/java/tech/ailef/dbadmin/external/controller/GlobalController.java b/src/main/java/tech/ailef/dbadmin/external/controller/GlobalController.java index dc52283..8ba6162 100644 --- a/src/main/java/tech/ailef/dbadmin/external/controller/GlobalController.java +++ b/src/main/java/tech/ailef/dbadmin/external/controller/GlobalController.java @@ -36,7 +36,6 @@ public class GlobalController { /** * The baseUrl as specified in the properties file by the user - * @param request * @return */ @ModelAttribute("baseUrl") @@ -44,11 +43,21 @@ public class GlobalController { return props.getBaseUrl(); } + /** + * The full request URL, not including the query string + * @param request + * @return + */ @ModelAttribute("requestUrl") public String getRequestUrl(HttpServletRequest request) { return request.getRequestURI(); } + /** + * The UserConfiguration object used to retrieve values specified + * in the settings table. + * @return + */ @ModelAttribute("userConf") public UserConfiguration getUserConf() { return userConf; diff --git a/src/main/java/tech/ailef/dbadmin/external/controller/rest/AutocompleteController.java b/src/main/java/tech/ailef/dbadmin/external/controller/rest/AutocompleteController.java index 7f42dca..9817fa4 100644 --- a/src/main/java/tech/ailef/dbadmin/external/controller/rest/AutocompleteController.java +++ b/src/main/java/tech/ailef/dbadmin/external/controller/rest/AutocompleteController.java @@ -28,6 +28,12 @@ public class AutocompleteController { @Autowired private DbAdminRepository repository; + /** + * Returns a list of entities from a given table that match an input query. + * @param className + * @param query + * @return + */ @GetMapping("/{className}") public ResponseEntity autocomplete(@PathVariable String className, @RequestParam String query) { DbObjectSchema schema = dbAdmin.findSchemaByClassName(className); diff --git a/src/main/java/tech/ailef/dbadmin/external/dbmapping/CustomJpaRepository.java b/src/main/java/tech/ailef/dbadmin/external/dbmapping/CustomJpaRepository.java index 702d725..90b32ef 100644 --- a/src/main/java/tech/ailef/dbadmin/external/dbmapping/CustomJpaRepository.java +++ b/src/main/java/tech/ailef/dbadmin/external/dbmapping/CustomJpaRepository.java @@ -80,6 +80,51 @@ public class CustomJpaRepository extends SimpleJpaRepository { .setFirstResult((page - 1) * pageSize).getResultList(); } + + @SuppressWarnings("unchecked") + public int update(DbObjectSchema schema, Map params, Map files) { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + + CriteriaUpdate update = cb.createCriteriaUpdate(schema.getJavaClass()); + + Root root = update.from(schema.getJavaClass()); + + for (DbField field : schema.getSortedFields()) { + if (field.isPrimaryKey()) continue; + + boolean keepValue = params.getOrDefault("__keep_" + field.getName(), "off").equals("on"); + if (keepValue) continue; + + String stringValue = params.get(field.getName()); + Object value = null; + if (stringValue != null && stringValue.isBlank()) stringValue = null; + if (stringValue != null) { + value = field.getType().parseValue(stringValue); + } else { + try { + MultipartFile file = files.get(field.getName()); + if (file != null) { + if (file.isEmpty()) value = null; + else value = file.getBytes(); + } + } catch (IOException e) { + throw new DbAdminException(e); + } + } + + if (field.getConnectedSchema() != null) + value = field.getConnectedSchema().getJpaRepository().findById(value).get(); + + update.set(root.get(field.getJavaName()), value); + } + + String pkName = schema.getPrimaryKey().getJavaName(); + update.where(cb.equal(root.get(pkName), params.get(schema.getPrimaryKey().getName()))); + + Query query = entityManager.createQuery(update); + return query.executeUpdate(); + } + @SuppressWarnings("unchecked") private List buildPredicates(String q, Set queryFilters, CriteriaBuilder cb, Path root) { @@ -155,48 +200,4 @@ public class CustomJpaRepository extends SimpleJpaRepository { } return finalPredicates; } - - @SuppressWarnings("unchecked") - public int update(DbObjectSchema schema, Map params, Map files) { - CriteriaBuilder cb = entityManager.getCriteriaBuilder(); - - CriteriaUpdate update = cb.createCriteriaUpdate(schema.getJavaClass()); - - Root root = update.from(schema.getJavaClass()); - - for (DbField field : schema.getSortedFields()) { - if (field.isPrimaryKey()) continue; - - boolean keepValue = params.getOrDefault("__keep_" + field.getName(), "off").equals("on"); - if (keepValue) continue; - - String stringValue = params.get(field.getName()); - Object value = null; - if (stringValue != null && stringValue.isBlank()) stringValue = null; - if (stringValue != null) { - value = field.getType().parseValue(stringValue); - } else { - try { - MultipartFile file = files.get(field.getName()); - if (file != null) { - if (file.isEmpty()) value = null; - else value = file.getBytes(); - } - } catch (IOException e) { - throw new DbAdminException(e); - } - } - - if (field.getConnectedSchema() != null) - value = field.getConnectedSchema().getJpaRepository().findById(value).get(); - - update.set(root.get(field.getJavaName()), value); - } - - String pkName = schema.getPrimaryKey().getJavaName(); - update.where(cb.equal(root.get(pkName), params.get(schema.getPrimaryKey().getName()))); - - Query query = entityManager.createQuery(update); - return query.executeUpdate(); - } } diff --git a/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbAdminRepository.java b/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbAdminRepository.java index d8d802d..6f22b46 100644 --- a/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbAdminRepository.java +++ b/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbAdminRepository.java @@ -256,7 +256,6 @@ public class DbAdminRepository { * Delete a specific object * @param schema * @param id - * @return */ @SuppressWarnings("unchecked") @Transactional("transactionManager") diff --git a/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbField.java b/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbField.java index 6e5d4c5..2161958 100644 --- a/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbField.java +++ b/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbField.java @@ -13,13 +13,25 @@ import tech.ailef.dbadmin.external.annotations.Filterable; import tech.ailef.dbadmin.external.annotations.FilterableType; public class DbField { + /** + * The inferred name of this field on the database + */ protected String dbName; + /** + * The name of this field in the Java code (instance variable) + */ protected String javaName; + /** + * The type of this field + */ protected DbFieldType type; @JsonIgnore + /** + * The primitive Field object from the Class + */ protected Field field; /** @@ -29,12 +41,25 @@ public class DbField { @JsonIgnore private Class connectedType; + /** + * Whether this field is a primary key + */ private boolean primaryKey; + /** + * Whether this field is nullable + */ private boolean nullable; + /** + * The optional format to apply to this field, if the `@DisplayFormat` + * annotation has been applied. + */ private String format; + /** + * The schema this field belongs to + */ @JsonIgnore private DbObjectSchema schema; diff --git a/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbFieldType.java b/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbFieldType.java index 8cf1634..3f4eaaf 100644 --- a/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbFieldType.java +++ b/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbFieldType.java @@ -14,6 +14,9 @@ import jakarta.persistence.OneToOne; import tech.ailef.dbadmin.external.dto.CompareOperator; import tech.ailef.dbadmin.external.exceptions.DbAdminException; +/** + * The list of supported field types + */ public enum DbFieldType { INTEGER { @Override @@ -27,7 +30,6 @@ public enum DbFieldType { } @Override - public Class getJavaClass() { return Integer.class; } diff --git a/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbFieldValue.java b/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbFieldValue.java index 6575818..bea6da3 100644 --- a/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbFieldValue.java +++ b/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbFieldValue.java @@ -4,6 +4,10 @@ import java.util.Objects; import com.fasterxml.jackson.annotation.JsonIgnore; +/** + * Wrapper for the value of a field + * + */ public class DbFieldValue { private Object value; diff --git a/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbObject.java b/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbObject.java index 0da11bc..42021b9 100644 --- a/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbObject.java +++ b/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbObject.java @@ -15,9 +15,19 @@ import tech.ailef.dbadmin.external.annotations.DisplayName; import tech.ailef.dbadmin.external.exceptions.DbAdminException; import tech.ailef.dbadmin.external.misc.Utils; +/** + * Wrapper for all objects retrieved from the database. + * + */ public class DbObject { + /** + * The instance of the object, i.e. an instance of the `@Entity` class + */ private Object instance; + /** + * The schema this object belongs to + */ private DbObjectSchema schema; public DbObject(Object instance, DbObjectSchema schema) { diff --git a/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbObjectSchema.java b/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbObjectSchema.java index 6b389bd..4ef961a 100644 --- a/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbObjectSchema.java +++ b/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbObjectSchema.java @@ -22,6 +22,11 @@ import tech.ailef.dbadmin.external.annotations.ComputedColumn; import tech.ailef.dbadmin.external.exceptions.DbAdminException; import tech.ailef.dbadmin.external.misc.Utils; +/** + * A class that represents a table/`@Entity` as reconstructed from the + * JPA annotations found on its fields. + * + */ public class DbObjectSchema { /** * All the fields in this table. The fields include all the @@ -54,6 +59,13 @@ public class DbObjectSchema { */ private String tableName; + /** + * Initializes this schema for the specific `@Entity` class. + * Determines the table name from the `@Table` annotation and also + * which methods are `@ComputedColumn`s + * @param klass the `@Entity` class + * @param dbAdmin the DbAdmin instance + */ public DbObjectSchema(Class klass, DbAdmin dbAdmin) { this.dbAdmin = dbAdmin; this.entityClass = klass; @@ -83,48 +95,103 @@ public class DbObjectSchema { } } + /** + * Returns the DbAdmin instance + * @return the DbAdmin instance + */ public DbAdmin getDbAdmin() { return dbAdmin; } + /** + * Returns the Java class for the underlying `@Entity` this schema + * corresponds to + * @return the Java class for the `@Entity` this schema corresponds to + */ @JsonIgnore public Class getJavaClass() { return entityClass; } + /** + * Returns the name of the Java class for the underlying `@Entity` this schema + * corresponds to + * @return the name of the Java class for the `@Entity` this schema corresponds to + */ @JsonIgnore public String getClassName() { return entityClass.getName(); } + /** + * Returns an unmodifiable list of all the fields in the schema + * @return an unmodifiable list of all the fields in the schema + */ public List getFields() { return Collections.unmodifiableList(fields); } + /** + * Get a field by its Java name, i.e. the name of the instance variable + * in the `@Entity` class + * @param name name of the instance variable + * @return the DbField if found, null otherwise + */ public DbField getFieldByJavaName(String name) { return fields.stream().filter(f -> f.getJavaName().equals(name)).findFirst().orElse(null); } + /** + * Get a field by its database name, i.e. the name of the column corresponding + * to the field + * @param name name of the column + * @return the DbField if found, null otherwise + */ public DbField getFieldByName(String name) { return fields.stream().filter(f -> f.getName().equals(name)).findFirst().orElse(null); } + /** + * Adds a field to this schema. This is used by the DbAdmin instance + * during initialization and it's not supposed to be called afterwards + * @param f the DbField to add + */ public void addField(DbField f) { fields.add(f); } - + + /** + * Returns the underlying CustomJpaRepository + * @return + */ public CustomJpaRepository getJpaRepository() { return jpaRepository; } + /** + * Sets the underlying CustomJpaRepository + * @param jpaRepository + */ public void setJpaRepository(CustomJpaRepository jpaRepository) { this.jpaRepository = jpaRepository; } + /** + * Returns the inferred table name for this schema + * @return + */ public String getTableName() { return tableName; } + /** + * Returns a sorted list of physical fields (i.e., fields that correspond to + * a column in the table as opposed to fields that are just present as + * instance variables, like relationship fields). Sorted alphabetically + * with priority to the primary key. + * + * @return + */ @JsonIgnore public List getSortedFields() { return getFields().stream() @@ -146,6 +213,10 @@ public class DbObjectSchema { }).collect(Collectors.toList()); } + /** + * Returns the list of relationship fields + * @return + */ public List getRelationshipFields() { List res = getFields().stream().filter(f -> { return f.getPrimitiveField().getAnnotation(OneToMany.class) != null @@ -154,6 +225,11 @@ public class DbObjectSchema { return res; } + /** + * Returns the list of ManyToMany fields owned by this class (i.e. they + * do not have "mappedBy") + * @return + */ public List getManyToManyOwnedFields() { List res = getFields().stream().filter(f -> { ManyToMany anno = f.getPrimitiveField().getAnnotation(ManyToMany.class); @@ -162,6 +238,10 @@ public class DbObjectSchema { return res; } + /** + * Returns the DbField which serves as the primary key for this schema + * @return + */ @JsonIgnore public DbField getPrimaryKey() { Optional pk = fields.stream().filter(f -> f.isPrimaryKey()).findFirst(); @@ -171,21 +251,37 @@ public class DbObjectSchema { throw new RuntimeException("No primary key defined on " + entityClass.getName() + " (table `" + tableName + "`)"); } + /** + * Returns the names of the `@ComputedColumn`s in this schema + * @return + */ public List getComputedColumnNames() { return computedColumns.keySet().stream().sorted().toList(); } + /** + * Returns the method for the given `@ComputedColumn` name + * @param name the name of the `@ComputedColumn` + * @return the corresponding instance method if found, null otherwise + */ public Method getComputedColumn(String name) { return computedColumns.get(name); } + /** + * Returns the list of fields that are `@Filterable` + * @return + */ public List getFilterableFields() { return getSortedFields().stream().filter(f -> { - return !f.isBinary() && !f.isPrimaryKey() - && f.isFilterable(); + return !f.isBinary() && !f.isPrimaryKey() && f.isFilterable(); }).toList(); } + /** + * Returns all the data in this schema, as `DbObject`s + * @return + */ public List findAll() { List r = jpaRepository.findAll(); return r.stream().map(o -> new DbObject(o, this)).toList(); diff --git a/src/main/java/tech/ailef/dbadmin/external/dto/AutocompleteSearchResult.java b/src/main/java/tech/ailef/dbadmin/external/dto/AutocompleteSearchResult.java index a714bd0..7701972 100644 --- a/src/main/java/tech/ailef/dbadmin/external/dto/AutocompleteSearchResult.java +++ b/src/main/java/tech/ailef/dbadmin/external/dto/AutocompleteSearchResult.java @@ -2,6 +2,11 @@ package tech.ailef.dbadmin.external.dto; import tech.ailef.dbadmin.external.dbmapping.DbObject; +/** + * An object to hold autocomplete results returned from the + * respective AutocompleteController + * + */ public class AutocompleteSearchResult { private Object id; diff --git a/src/main/java/tech/ailef/dbadmin/external/dto/CompareOperator.java b/src/main/java/tech/ailef/dbadmin/external/dto/CompareOperator.java index fbeb373..bc75990 100644 --- a/src/main/java/tech/ailef/dbadmin/external/dto/CompareOperator.java +++ b/src/main/java/tech/ailef/dbadmin/external/dto/CompareOperator.java @@ -1,5 +1,9 @@ package tech.ailef.dbadmin.external.dto; +/** + * A list of operators that are used in faceted search. + * + */ public enum CompareOperator { GT { @Override diff --git a/src/main/java/tech/ailef/dbadmin/external/dto/LogsSearchRequest.java b/src/main/java/tech/ailef/dbadmin/external/dto/LogsSearchRequest.java index e766731..3d66a3a 100644 --- a/src/main/java/tech/ailef/dbadmin/external/dto/LogsSearchRequest.java +++ b/src/main/java/tech/ailef/dbadmin/external/dto/LogsSearchRequest.java @@ -3,19 +3,45 @@ package tech.ailef.dbadmin.external.dto; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; +/** + * A client request for the Action logs page where + * several filtering parameters are present + * + */ public class LogsSearchRequest { + /** + * The table name to filter on + */ private String table; + /** + * The action type to filter on (EDIT, CREATE, DELETE, ANY) + */ private String actionType; + /** + * The item id to filter on. + */ private String itemId; + /** + * The requested page + */ private int page; + /** + * The requested page size + */ private int pageSize; + /** + * The requested sort key + */ private String sortKey; + /** + * The requested sort order + */ private String sortOrder; public String getTable() { @@ -80,6 +106,10 @@ public class LogsSearchRequest { + page + ", pageSize=" + pageSize + ", sortKey=" + sortKey + ", sortOrder=" + sortOrder + "]"; } + /** + * Build a Spring PageRequest object from the parameters in this request + * @return a Spring PageRequest object + */ public PageRequest toPageRequest() { int actualPage = page - 1 < 0 ? 0 : page - 1; int actualPageSize = pageSize <= 0 ? 50 : pageSize; diff --git a/src/main/java/tech/ailef/dbadmin/external/dto/PaginatedResult.java b/src/main/java/tech/ailef/dbadmin/external/dto/PaginatedResult.java index 2b22fda..d389fab 100644 --- a/src/main/java/tech/ailef/dbadmin/external/dto/PaginatedResult.java +++ b/src/main/java/tech/ailef/dbadmin/external/dto/PaginatedResult.java @@ -2,9 +2,19 @@ package tech.ailef.dbadmin.external.dto; import java.util.List; +/** + * A wrapper class that holds info about the current pagination and one page + * of returned result. + */ public class PaginatedResult { + /** + * The pagination settings used to produce this output + */ private PaginationInfo pagination; + /** + * The list of results in the current page + */ private List results; public PaginatedResult(PaginationInfo pagination, List page) { diff --git a/src/main/java/tech/ailef/dbadmin/external/dto/QueryFilter.java b/src/main/java/tech/ailef/dbadmin/external/dto/QueryFilter.java index 9449516..1f252c6 100644 --- a/src/main/java/tech/ailef/dbadmin/external/dto/QueryFilter.java +++ b/src/main/java/tech/ailef/dbadmin/external/dto/QueryFilter.java @@ -29,6 +29,10 @@ public class QueryFilter { return value; } + /** + * Provides a readable version of this query filter, customized + * based on field type and/or operator. + */ @Override public String toString() { if (value != null && !value.toString().isBlank()) { diff --git a/src/main/java/tech/ailef/dbadmin/external/exceptions/DbAdminException.java b/src/main/java/tech/ailef/dbadmin/external/exceptions/DbAdminException.java index 4c97285..1953c3e 100644 --- a/src/main/java/tech/ailef/dbadmin/external/exceptions/DbAdminException.java +++ b/src/main/java/tech/ailef/dbadmin/external/exceptions/DbAdminException.java @@ -1,5 +1,9 @@ package tech.ailef.dbadmin.external.exceptions; +/** + * Generic top-level exception for everything thrown by us + * + */ public class DbAdminException extends RuntimeException { private static final long serialVersionUID = 8120227031645804467L; diff --git a/src/main/java/tech/ailef/dbadmin/external/misc/Utils.java b/src/main/java/tech/ailef/dbadmin/external/misc/Utils.java index 7fcfc93..24ec730 100644 --- a/src/main/java/tech/ailef/dbadmin/external/misc/Utils.java +++ b/src/main/java/tech/ailef/dbadmin/external/misc/Utils.java @@ -1,6 +1,7 @@ package tech.ailef.dbadmin.external.misc; import java.util.ArrayList; + import java.util.HashSet; import java.util.List; import java.util.Set; @@ -13,7 +14,38 @@ import tech.ailef.dbadmin.external.dto.CompareOperator; import tech.ailef.dbadmin.external.dto.QueryFilter; import tech.ailef.dbadmin.external.exceptions.DbAdminException; +/** + * Collection of utility functions used across the project + * + */ public interface Utils { + /** + * Converts snake case to camel case + * @param text + * @return + */ + 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(); + } + + /** + * Convers camel case to snake case + * @param v + * @return + */ public static String camelToSnake(String v) { if (Character.isUpperCase(v.charAt(0))) { v = Character.toLowerCase(v.charAt(0)) + v.substring(1); @@ -23,6 +55,12 @@ public interface Utils { } + /** + * Converts a set of query filters applied with the faceted search feature + * to a multi value map + * @param filters + * @return + */ public static MultiValueMap computeParams(Set filters) { MultiValueMap r = new LinkedMultiValueMap<>(); if (filters == null) @@ -41,6 +79,13 @@ public interface Utils { return r; } + /** + * Converts a multi value map of parameters containing query filters applied + * with the faceted search feature into a set of QueryFilter objects + * @param schema + * @param params + * @return + */ public static Set computeFilters(DbObjectSchema schema, MultiValueMap params) { if (params == null) return new HashSet<>(); @@ -85,20 +130,4 @@ public interface Utils { 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(); - } }