| @@ -0,0 +1,37 @@ | |||||
| HELP.md | |||||
| .gradle | |||||
| build/ | |||||
| !gradle/wrapper/gradle-wrapper.jar | |||||
| !**/src/main/**/build/ | |||||
| !**/src/test/**/build/ | |||||
| ### STS ### | |||||
| .apt_generated | |||||
| .classpath | |||||
| .factorypath | |||||
| .project | |||||
| .settings | |||||
| .springBeans | |||||
| .sts4-cache | |||||
| bin/ | |||||
| !**/src/main/**/bin/ | |||||
| !**/src/test/**/bin/ | |||||
| ### IntelliJ IDEA ### | |||||
| .idea | |||||
| *.iws | |||||
| *.iml | |||||
| *.ipr | |||||
| out/ | |||||
| !**/src/main/**/out/ | |||||
| !**/src/test/**/out/ | |||||
| ### NetBeans ### | |||||
| /nbproject/private/ | |||||
| /nbbuild/ | |||||
| /dist/ | |||||
| /nbdist/ | |||||
| /.nb-gradle/ | |||||
| ### VS Code ### | |||||
| .vscode/ | |||||
| @@ -0,0 +1,56 @@ | |||||
| plugins { | |||||
| id 'java' | |||||
| id 'org.springframework.boot' version '3.1.1' | |||||
| id 'io.spring.dependency-management' version '1.1.0' | |||||
| } | |||||
| group = 'com.ffii' | |||||
| version = '0.0.1-SNAPSHOT' | |||||
| java { | |||||
| sourceCompatibility = '17' | |||||
| } | |||||
| repositories { | |||||
| mavenCentral() | |||||
| } | |||||
| dependencies { | |||||
| implementation 'org.springframework.boot:spring-boot-starter-data-jpa' | |||||
| implementation 'org.springframework.boot:spring-boot-starter-data-ldap' | |||||
| implementation 'org.springframework.boot:spring-boot-starter-mail' | |||||
| implementation 'org.springframework.boot:spring-boot-starter-security' | |||||
| implementation 'org.springframework.boot:spring-boot-starter-web' | |||||
| implementation 'org.springframework.boot:spring-boot-starter-validation' | |||||
| implementation 'org.springframework.boot:spring-boot-starter-log4j2' | |||||
| implementation 'org.springframework.security:spring-security-ldap' | |||||
| implementation 'org.liquibase:liquibase-core' | |||||
| implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.12.0' | |||||
| implementation group: 'org.apache.poi', name: 'poi', version: '5.2.3' | |||||
| implementation group: 'org.apache.poi', name: 'poi-ooxml', version: '5.2.3' | |||||
| implementation group: 'jakarta.persistence', name: 'jakarta.persistence-api', version: '3.1.0' | |||||
| implementation group: 'jakarta.annotation', name: 'jakarta.annotation-api', version: '2.1.1' | |||||
| implementation group: 'jakarta.validation', name: 'jakarta.validation-api', version: '3.0.2' | |||||
| implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.15.2' | |||||
| implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.15.2' | |||||
| implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5' | |||||
| implementation group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5' | |||||
| implementation group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5' | |||||
| compileOnly group: 'jakarta.servlet', name: 'jakarta.servlet-api', version: '6.0.0' | |||||
| runtimeOnly 'com.mysql:mysql-connector-j' | |||||
| runtimeOnly 'com.unboundid:unboundid-ldapsdk:6.0.9' | |||||
| testImplementation 'org.springframework.boot:spring-boot-starter-test' | |||||
| testImplementation 'org.springframework.security:spring-security-test' | |||||
| } | |||||
| configurations { | |||||
| all { | |||||
| exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging' | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,6 @@ | |||||
| distributionBase=GRADLE_USER_HOME | |||||
| distributionPath=wrapper/dists | |||||
| distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip | |||||
| networkTimeout=10000 | |||||
| zipStoreBase=GRADLE_USER_HOME | |||||
| zipStorePath=wrapper/dists | |||||
| @@ -0,0 +1,240 @@ | |||||
| #!/bin/sh | |||||
| # | |||||
| # Copyright © 2015-2021 the original authors. | |||||
| # | |||||
| # Licensed 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. | |||||
| # | |||||
| ############################################################################## | |||||
| # | |||||
| # Gradle start up script for POSIX generated by Gradle. | |||||
| # | |||||
| # Important for running: | |||||
| # | |||||
| # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is | |||||
| # noncompliant, but you have some other compliant shell such as ksh or | |||||
| # bash, then to run this script, type that shell name before the whole | |||||
| # command line, like: | |||||
| # | |||||
| # ksh Gradle | |||||
| # | |||||
| # Busybox and similar reduced shells will NOT work, because this script | |||||
| # requires all of these POSIX shell features: | |||||
| # * functions; | |||||
| # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», | |||||
| # «${var#prefix}», «${var%suffix}», and «$( cmd )»; | |||||
| # * compound commands having a testable exit status, especially «case»; | |||||
| # * various built-in commands including «command», «set», and «ulimit». | |||||
| # | |||||
| # Important for patching: | |||||
| # | |||||
| # (2) This script targets any POSIX shell, so it avoids extensions provided | |||||
| # by Bash, Ksh, etc; in particular arrays are avoided. | |||||
| # | |||||
| # The "traditional" practice of packing multiple parameters into a | |||||
| # space-separated string is a well documented source of bugs and security | |||||
| # problems, so this is (mostly) avoided, by progressively accumulating | |||||
| # options in "$@", and eventually passing that to Java. | |||||
| # | |||||
| # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, | |||||
| # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; | |||||
| # see the in-line comments for details. | |||||
| # | |||||
| # There are tweaks for specific operating systems such as AIX, CygWin, | |||||
| # Darwin, MinGW, and NonStop. | |||||
| # | |||||
| # (3) This script is generated from the Groovy template | |||||
| # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt | |||||
| # within the Gradle project. | |||||
| # | |||||
| # You can find Gradle at https://github.com/gradle/gradle/. | |||||
| # | |||||
| ############################################################################## | |||||
| # Attempt to set APP_HOME | |||||
| # Resolve links: $0 may be a link | |||||
| app_path=$0 | |||||
| # Need this for daisy-chained symlinks. | |||||
| while | |||||
| APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path | |||||
| [ -h "$app_path" ] | |||||
| do | |||||
| ls=$( ls -ld "$app_path" ) | |||||
| link=${ls#*' -> '} | |||||
| case $link in #( | |||||
| /*) app_path=$link ;; #( | |||||
| *) app_path=$APP_HOME$link ;; | |||||
| esac | |||||
| done | |||||
| APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit | |||||
| APP_NAME="Gradle" | |||||
| APP_BASE_NAME=${0##*/} | |||||
| # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. | |||||
| DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' | |||||
| # Use the maximum available, or set MAX_FD != -1 to use that value. | |||||
| MAX_FD=maximum | |||||
| warn () { | |||||
| echo "$*" | |||||
| } >&2 | |||||
| die () { | |||||
| echo | |||||
| echo "$*" | |||||
| echo | |||||
| exit 1 | |||||
| } >&2 | |||||
| # OS specific support (must be 'true' or 'false'). | |||||
| cygwin=false | |||||
| msys=false | |||||
| darwin=false | |||||
| nonstop=false | |||||
| case "$( uname )" in #( | |||||
| CYGWIN* ) cygwin=true ;; #( | |||||
| Darwin* ) darwin=true ;; #( | |||||
| MSYS* | MINGW* ) msys=true ;; #( | |||||
| NONSTOP* ) nonstop=true ;; | |||||
| esac | |||||
| CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar | |||||
| # Determine the Java command to use to start the JVM. | |||||
| 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 | |||||
| if [ ! -x "$JAVACMD" ] ; then | |||||
| die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME | |||||
| Please set the JAVA_HOME variable in your environment to match the | |||||
| location of your Java installation." | |||||
| fi | |||||
| else | |||||
| JAVACMD=java | |||||
| which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. | |||||
| Please set the JAVA_HOME variable in your environment to match the | |||||
| location of your Java installation." | |||||
| fi | |||||
| # Increase the maximum file descriptors if we can. | |||||
| if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then | |||||
| case $MAX_FD in #( | |||||
| max*) | |||||
| MAX_FD=$( ulimit -H -n ) || | |||||
| warn "Could not query maximum file descriptor limit" | |||||
| esac | |||||
| case $MAX_FD in #( | |||||
| '' | soft) :;; #( | |||||
| *) | |||||
| ulimit -n "$MAX_FD" || | |||||
| warn "Could not set maximum file descriptor limit to $MAX_FD" | |||||
| esac | |||||
| fi | |||||
| # Collect all arguments for the java command, stacking in reverse order: | |||||
| # * args from the command line | |||||
| # * the main class name | |||||
| # * -classpath | |||||
| # * -D...appname settings | |||||
| # * --module-path (only if needed) | |||||
| # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. | |||||
| # For Cygwin or MSYS, switch paths to Windows format before running java | |||||
| if "$cygwin" || "$msys" ; then | |||||
| APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) | |||||
| CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) | |||||
| JAVACMD=$( cygpath --unix "$JAVACMD" ) | |||||
| # Now convert the arguments - kludge to limit ourselves to /bin/sh | |||||
| for arg do | |||||
| if | |||||
| case $arg in #( | |||||
| -*) false ;; # don't mess with options #( | |||||
| /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath | |||||
| [ -e "$t" ] ;; #( | |||||
| *) false ;; | |||||
| esac | |||||
| then | |||||
| arg=$( cygpath --path --ignore --mixed "$arg" ) | |||||
| fi | |||||
| # Roll the args list around exactly as many times as the number of | |||||
| # args, so each arg winds up back in the position where it started, but | |||||
| # possibly modified. | |||||
| # | |||||
| # NB: a `for` loop captures its iteration list before it begins, so | |||||
| # changing the positional parameters here affects neither the number of | |||||
| # iterations, nor the values presented in `arg`. | |||||
| shift # remove old arg | |||||
| set -- "$@" "$arg" # push replacement arg | |||||
| done | |||||
| fi | |||||
| # Collect all arguments for the java command; | |||||
| # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of | |||||
| # shell script including quotes and variable substitutions, so put them in | |||||
| # double quotes to make sure that they get re-expanded; and | |||||
| # * put everything else in single quotes, so that it's not re-expanded. | |||||
| set -- \ | |||||
| "-Dorg.gradle.appname=$APP_BASE_NAME" \ | |||||
| -classpath "$CLASSPATH" \ | |||||
| org.gradle.wrapper.GradleWrapperMain \ | |||||
| "$@" | |||||
| # Stop when "xargs" is not available. | |||||
| if ! command -v xargs >/dev/null 2>&1 | |||||
| then | |||||
| die "xargs is not available" | |||||
| fi | |||||
| # Use "xargs" to parse quoted args. | |||||
| # | |||||
| # With -n1 it outputs one arg per line, with the quotes and backslashes removed. | |||||
| # | |||||
| # In Bash we could simply go: | |||||
| # | |||||
| # readarray ARGS < <( xargs -n1 <<<"$var" ) && | |||||
| # set -- "${ARGS[@]}" "$@" | |||||
| # | |||||
| # but POSIX shell has neither arrays nor command substitution, so instead we | |||||
| # post-process each arg (as a line of input to sed) to backslash-escape any | |||||
| # character that might be a shell metacharacter, then use eval to reverse | |||||
| # that process (while maintaining the separation between arguments), and wrap | |||||
| # the whole thing up as a single "set" statement. | |||||
| # | |||||
| # This will of course break if any of these variables contains a newline or | |||||
| # an unmatched quote. | |||||
| # | |||||
| eval "set -- $( | |||||
| printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | | |||||
| xargs -n1 | | |||||
| sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | | |||||
| tr '\n' ' ' | |||||
| )" '"$@"' | |||||
| exec "$JAVACMD" "$@" | |||||
| @@ -0,0 +1,91 @@ | |||||
| @rem | |||||
| @rem Copyright 2015 the original author or authors. | |||||
| @rem | |||||
| @rem Licensed under the Apache License, Version 2.0 (the "License"); | |||||
| @rem you may not use this file except in compliance with the License. | |||||
| @rem You may obtain a copy of the License at | |||||
| @rem | |||||
| @rem https://www.apache.org/licenses/LICENSE-2.0 | |||||
| @rem | |||||
| @rem Unless required by applicable law or agreed to in writing, software | |||||
| @rem distributed under the License is distributed on an "AS IS" BASIS, | |||||
| @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
| @rem See the License for the specific language governing permissions and | |||||
| @rem limitations under the License. | |||||
| @rem | |||||
| @if "%DEBUG%"=="" @echo off | |||||
| @rem ########################################################################## | |||||
| @rem | |||||
| @rem Gradle startup script for Windows | |||||
| @rem | |||||
| @rem ########################################################################## | |||||
| @rem Set local scope for the variables with windows NT shell | |||||
| if "%OS%"=="Windows_NT" setlocal | |||||
| set DIRNAME=%~dp0 | |||||
| if "%DIRNAME%"=="" set DIRNAME=. | |||||
| set APP_BASE_NAME=%~n0 | |||||
| set APP_HOME=%DIRNAME% | |||||
| @rem Resolve any "." and ".." in APP_HOME to make it shorter. | |||||
| for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi | |||||
| @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. | |||||
| set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" | |||||
| @rem Find java.exe | |||||
| if defined JAVA_HOME goto findJavaFromJavaHome | |||||
| set JAVA_EXE=java.exe | |||||
| %JAVA_EXE% -version >NUL 2>&1 | |||||
| if %ERRORLEVEL% equ 0 goto execute | |||||
| echo. | |||||
| echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. | |||||
| echo. | |||||
| echo Please set the JAVA_HOME variable in your environment to match the | |||||
| echo location of your Java installation. | |||||
| goto fail | |||||
| :findJavaFromJavaHome | |||||
| set JAVA_HOME=%JAVA_HOME:"=% | |||||
| set JAVA_EXE=%JAVA_HOME%/bin/java.exe | |||||
| if exist "%JAVA_EXE%" goto execute | |||||
| echo. | |||||
| echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% | |||||
| echo. | |||||
| echo Please set the JAVA_HOME variable in your environment to match the | |||||
| echo location of your Java installation. | |||||
| goto fail | |||||
| :execute | |||||
| @rem Setup the command line | |||||
| set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar | |||||
| @rem Execute Gradle | |||||
| "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* | |||||
| :end | |||||
| @rem End local scope for the variables with windows NT shell | |||||
| if %ERRORLEVEL% equ 0 goto mainEnd | |||||
| :fail | |||||
| rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of | |||||
| rem the _cmd.exe /c_ return code! | |||||
| set EXIT_CODE=%ERRORLEVEL% | |||||
| if %EXIT_CODE% equ 0 set EXIT_CODE=1 | |||||
| if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% | |||||
| exit /b %EXIT_CODE% | |||||
| :mainEnd | |||||
| if "%OS%"=="Windows_NT" endlocal | |||||
| :omega | |||||
| @@ -0,0 +1 @@ | |||||
| rootProject.name = 'TSMS' | |||||
| @@ -0,0 +1,121 @@ | |||||
| package com.ffii.core.entity; | |||||
| import java.io.Serializable; | |||||
| import java.time.LocalDateTime; | |||||
| import java.util.Optional; | |||||
| import jakarta.persistence.Column; | |||||
| import jakarta.persistence.MappedSuperclass; | |||||
| import jakarta.persistence.PrePersist; | |||||
| import jakarta.persistence.PreUpdate; | |||||
| import jakarta.persistence.Version; | |||||
| import jakarta.validation.constraints.NotNull; | |||||
| import org.springframework.security.core.context.SecurityContextHolder; | |||||
| /** @author Terence */ | |||||
| @MappedSuperclass | |||||
| public abstract class BaseEntity<ID extends Serializable> extends IdEntity<ID> { | |||||
| @NotNull | |||||
| @Version | |||||
| @Column | |||||
| private Integer version; | |||||
| @NotNull | |||||
| @Column(updatable = false) | |||||
| private LocalDateTime created; | |||||
| @Column(updatable = false) | |||||
| private String createdBy; | |||||
| @NotNull | |||||
| @Column | |||||
| private LocalDateTime modified; | |||||
| @Column | |||||
| private String modifiedBy; | |||||
| @NotNull | |||||
| @Column | |||||
| private Boolean deleted; | |||||
| @PrePersist | |||||
| public void autoSetCreated() { | |||||
| this.setCreated(LocalDateTime.now()); | |||||
| this.setModified(LocalDateTime.now()); | |||||
| this.setDeleted(Boolean.FALSE); | |||||
| Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) | |||||
| .ifPresentOrElse( | |||||
| authentication -> { | |||||
| this.setCreatedBy(authentication.getName()); | |||||
| this.setModifiedBy(authentication.getName()); | |||||
| }, | |||||
| () -> { | |||||
| this.setCreatedBy(null); | |||||
| this.setModifiedBy(null); | |||||
| }); | |||||
| } | |||||
| @PreUpdate | |||||
| public void autoSetModified() { | |||||
| this.setModified(LocalDateTime.now()); | |||||
| Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()).ifPresentOrElse( | |||||
| authentication -> this.setModifiedBy(authentication.getName()), | |||||
| () -> this.setModifiedBy(null)); | |||||
| } | |||||
| public Integer getVersion() { | |||||
| return this.version; | |||||
| } | |||||
| public void setVersion(Integer version) { | |||||
| this.version = version; | |||||
| } | |||||
| public LocalDateTime getCreated() { | |||||
| return this.created; | |||||
| } | |||||
| public void setCreated(LocalDateTime created) { | |||||
| this.created = created; | |||||
| } | |||||
| public String getCreatedBy() { | |||||
| return this.createdBy; | |||||
| } | |||||
| public void setCreatedBy(String createdBy) { | |||||
| this.createdBy = createdBy; | |||||
| } | |||||
| public LocalDateTime getModified() { | |||||
| return this.modified; | |||||
| } | |||||
| public void setModified(LocalDateTime modified) { | |||||
| this.modified = modified; | |||||
| } | |||||
| public String getModifiedBy() { | |||||
| return this.modifiedBy; | |||||
| } | |||||
| public void setModifiedBy(String modifiedBy) { | |||||
| this.modifiedBy = modifiedBy; | |||||
| } | |||||
| public Boolean isDeleted() { | |||||
| return this.deleted; | |||||
| } | |||||
| public Boolean getDeleted() { | |||||
| return this.deleted; | |||||
| } | |||||
| public void setDeleted(Boolean deleted) { | |||||
| this.deleted = deleted; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,49 @@ | |||||
| package com.ffii.core.entity; | |||||
| import java.io.Serializable; | |||||
| import jakarta.persistence.GeneratedValue; | |||||
| import jakarta.persistence.GenerationType; | |||||
| import jakarta.persistence.Id; | |||||
| import jakarta.persistence.MappedSuperclass; | |||||
| import jakarta.persistence.PostLoad; | |||||
| import jakarta.persistence.PrePersist; | |||||
| import jakarta.persistence.Transient; | |||||
| import org.springframework.data.domain.Persistable; | |||||
| import com.fasterxml.jackson.annotation.JsonIgnore; | |||||
| /** @author Terence */ | |||||
| @MappedSuperclass | |||||
| public abstract class IdEntity<ID extends Serializable> implements Persistable<ID> { | |||||
| @Id | |||||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | |||||
| private ID id; | |||||
| @Transient | |||||
| private boolean isNew = true; | |||||
| @JsonIgnore | |||||
| @Override | |||||
| public boolean isNew() { | |||||
| return isNew; | |||||
| } | |||||
| @PrePersist | |||||
| @PostLoad | |||||
| void markNotNew() { | |||||
| this.isNew = false; | |||||
| } | |||||
| // getter and setter | |||||
| public ID getId() { | |||||
| return id; | |||||
| } | |||||
| public void setId(ID id) { | |||||
| this.id = id; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,15 @@ | |||||
| package com.ffii.core.exception; | |||||
| import org.springframework.http.HttpStatus; | |||||
| import org.springframework.web.server.ResponseStatusException; | |||||
| public class BadRequestException extends ResponseStatusException { | |||||
| public BadRequestException() { | |||||
| super(HttpStatus.BAD_REQUEST); | |||||
| } | |||||
| public BadRequestException(String reason) { | |||||
| super(HttpStatus.BAD_REQUEST, reason); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,16 @@ | |||||
| package com.ffii.core.exception; | |||||
| import org.springframework.http.HttpStatus; | |||||
| import org.springframework.web.server.ResponseStatusException; | |||||
| /* e.g. sub record not under record */ | |||||
| public class ConflictException extends ResponseStatusException { | |||||
| public ConflictException() { | |||||
| super(HttpStatus.CONFLICT); | |||||
| } | |||||
| public ConflictException(String reason) { | |||||
| super(HttpStatus.CONFLICT, reason); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,19 @@ | |||||
| package com.ffii.core.exception; | |||||
| import org.springframework.http.HttpStatus; | |||||
| import org.springframework.web.server.ResponseStatusException; | |||||
| public class InternalServerErrorException extends ResponseStatusException { | |||||
| public InternalServerErrorException() { | |||||
| super(HttpStatus.INTERNAL_SERVER_ERROR); | |||||
| } | |||||
| public InternalServerErrorException(String reason) { | |||||
| super(HttpStatus.INTERNAL_SERVER_ERROR, reason); | |||||
| } | |||||
| public InternalServerErrorException(String reason, Throwable e) { | |||||
| super(HttpStatus.INTERNAL_SERVER_ERROR, reason, e); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,13 @@ | |||||
| package com.ffii.core.exception; | |||||
| import org.springframework.http.HttpStatus; | |||||
| import org.springframework.web.server.ResponseStatusException; | |||||
| /* main record not found (e.g. item record) */ | |||||
| public class NotFoundException extends ResponseStatusException{ | |||||
| public NotFoundException() { | |||||
| super(HttpStatus.NOT_FOUND); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,37 @@ | |||||
| package com.ffii.core.exception; | |||||
| import java.util.Map; | |||||
| import jakarta.validation.constraints.NotNull; | |||||
| import org.springframework.http.HttpStatus; | |||||
| import org.springframework.web.server.ResponseStatusException; | |||||
| import com.fasterxml.jackson.core.JsonProcessingException; | |||||
| import com.fasterxml.jackson.databind.ObjectMapper; | |||||
| /* sub record not found (e.g. item_line record) */ | |||||
| public class UnprocessableEntityException extends ResponseStatusException { | |||||
| public UnprocessableEntityException() { | |||||
| super(HttpStatus.UNPROCESSABLE_ENTITY); | |||||
| } | |||||
| public UnprocessableEntityException(@NotNull Map<String, Object> map) { | |||||
| super(HttpStatus.UNPROCESSABLE_ENTITY, map2Str(map)); | |||||
| } | |||||
| public UnprocessableEntityException(String reason) { | |||||
| super(HttpStatus.UNPROCESSABLE_ENTITY, reason); | |||||
| } | |||||
| private static String map2Str(@NotNull Map<String, Object> map) { | |||||
| try { | |||||
| return new ObjectMapper().writeValueAsString(map); | |||||
| } catch (JsonProcessingException e) { | |||||
| e.printStackTrace(); | |||||
| return ""; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,21 @@ | |||||
| package com.ffii.core.response; | |||||
| public class DataRes<T> { | |||||
| private T data; | |||||
| public DataRes() { | |||||
| } | |||||
| public DataRes(T data) { | |||||
| this.data = data; | |||||
| } | |||||
| public T getData() { | |||||
| return data; | |||||
| } | |||||
| public void setData(T data) { | |||||
| this.data = data; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,36 @@ | |||||
| package com.ffii.core.response; | |||||
| import java.time.LocalDateTime; | |||||
| public class ErrorRes { | |||||
| private LocalDateTime timestamp; | |||||
| private String traceId; | |||||
| public ErrorRes() { | |||||
| this.timestamp = LocalDateTime.now(); | |||||
| } | |||||
| public ErrorRes(String traceId) { | |||||
| this.timestamp = LocalDateTime.now(); | |||||
| this.traceId = traceId; | |||||
| } | |||||
| public LocalDateTime getTimestamp() { | |||||
| return timestamp; | |||||
| } | |||||
| public void setTimestamp(LocalDateTime timestamp) { | |||||
| this.timestamp = timestamp; | |||||
| } | |||||
| public String getTraceId() { | |||||
| return traceId; | |||||
| } | |||||
| public void setTraceId(String traceId) { | |||||
| this.traceId = traceId; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,39 @@ | |||||
| package com.ffii.core.response; | |||||
| import java.time.LocalDateTime; | |||||
| import com.fasterxml.jackson.annotation.JsonInclude; | |||||
| public class FailureRes { | |||||
| private LocalDateTime timestamp; | |||||
| @JsonInclude(JsonInclude.Include.NON_NULL) | |||||
| private String error; | |||||
| public FailureRes() { | |||||
| this.timestamp = LocalDateTime.now(); | |||||
| } | |||||
| public FailureRes(String error) { | |||||
| this.timestamp = LocalDateTime.now(); | |||||
| this.error = error; | |||||
| } | |||||
| public LocalDateTime getTimestamp() { | |||||
| return timestamp; | |||||
| } | |||||
| public void setTimestamp(LocalDateTime timestamp) { | |||||
| this.timestamp = timestamp; | |||||
| } | |||||
| public String getError() { | |||||
| return error; | |||||
| } | |||||
| public void setError(String error) { | |||||
| this.error = error; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,21 @@ | |||||
| package com.ffii.core.response; | |||||
| public class IdRes { | |||||
| private long id; | |||||
| public IdRes() { | |||||
| } | |||||
| public IdRes(long id) { | |||||
| this.id = id; | |||||
| } | |||||
| public long getId() { | |||||
| return id; | |||||
| } | |||||
| public void setId(long id) { | |||||
| this.id = id; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,41 @@ | |||||
| package com.ffii.core.response; | |||||
| import java.util.List; | |||||
| import com.fasterxml.jackson.annotation.JsonInclude; | |||||
| public class RecordsRes<T> { | |||||
| private List<T> records; | |||||
| @JsonInclude(JsonInclude.Include.NON_NULL) | |||||
| private Integer total; | |||||
| public RecordsRes() { | |||||
| } | |||||
| public RecordsRes(List<T> records) { | |||||
| this.records = records; | |||||
| } | |||||
| public RecordsRes(List<T> records, int total) { | |||||
| this.records = records; | |||||
| this.total = total; | |||||
| } | |||||
| public List<T> getRecords() { | |||||
| return records; | |||||
| } | |||||
| public void setRecords(List<T> records) { | |||||
| this.records = records; | |||||
| } | |||||
| public Integer getTotal() { | |||||
| return total; | |||||
| } | |||||
| public void setTotal(Integer total) { | |||||
| this.total = total; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,42 @@ | |||||
| package com.ffii.core.support; | |||||
| import java.io.Serializable; | |||||
| import java.util.Optional; | |||||
| import org.springframework.transaction.annotation.Transactional; | |||||
| import org.springframework.util.Assert; | |||||
| import com.ffii.core.entity.BaseEntity; | |||||
| import com.ffii.core.exception.ConflictException; | |||||
| /** @author Alex */ | |||||
| public abstract class AbstractBaseEntityService<T extends BaseEntity<ID>, ID extends Serializable, R extends AbstractRepository<T, ID>> | |||||
| extends AbstractIdEntityService<T, ID, R> { | |||||
| public AbstractBaseEntityService(JdbcDao jdbcDao, R repository) { | |||||
| super(jdbcDao, repository); | |||||
| } | |||||
| /** find and check versionId */ | |||||
| public Optional<T> find(ID id, int version) { | |||||
| Assert.notNull(id, "id must not be null"); | |||||
| return repository.findById(id) | |||||
| .map(entity -> { | |||||
| if (entity.getVersion() != version) throw new ConflictException("OPTIMISTIC_LOCK"); | |||||
| return entity; | |||||
| }); | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public void markDelete(ID id) { | |||||
| Assert.notNull(id, "id must not be null"); | |||||
| find(id).ifPresent(t -> markDelete(t)); | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public void markDelete(T entity) { | |||||
| Assert.notNull(entity, "entity must not be null"); | |||||
| entity.setDeleted(Boolean.TRUE); | |||||
| save(entity); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,65 @@ | |||||
| package com.ffii.core.support; | |||||
| import java.io.Serializable; | |||||
| import java.util.List; | |||||
| import java.util.Optional; | |||||
| import org.springframework.transaction.annotation.Transactional; | |||||
| import org.springframework.util.Assert; | |||||
| import com.ffii.core.entity.IdEntity; | |||||
| /** @author Alex */ | |||||
| public abstract class AbstractIdEntityService<T extends IdEntity<ID>, ID extends Serializable, R extends AbstractRepository<T, ID>> | |||||
| extends AbstractService { | |||||
| protected R repository; | |||||
| public AbstractIdEntityService(JdbcDao jdbcDao, R repository) { | |||||
| super(jdbcDao); | |||||
| this.repository = repository; | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public T save(T entity) { | |||||
| Assert.notNull(entity, "entity must not be null"); | |||||
| return this.repository.save(entity); | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public T saveAndFlush(T entity) { | |||||
| Assert.notNull(entity, "entity must not be null"); | |||||
| return this.repository.saveAndFlush(entity); | |||||
| } | |||||
| public List<T> listAll() { | |||||
| return this.repository.findAll(); | |||||
| } | |||||
| public Optional<T> find(ID id) { | |||||
| Assert.notNull(id, "id must not be null"); | |||||
| return this.repository.findById(id); | |||||
| } | |||||
| public boolean existsById(ID id) { | |||||
| Assert.notNull(id, "id must not be null"); | |||||
| return this.repository.existsById(id); | |||||
| } | |||||
| public List<T> findAllByIds(List<ID> ids) { | |||||
| Assert.notNull(ids, "ids must not be null"); | |||||
| return this.repository.findAllById(ids); | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public void delete(ID id) { | |||||
| Assert.notNull(id, "id must not be null"); | |||||
| this.repository.deleteById(id); | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public void delete(T entity) { | |||||
| Assert.notNull(entity, "entity must not be null"); | |||||
| this.repository.delete(entity); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,16 @@ | |||||
| package com.ffii.core.support; | |||||
| import java.io.Serializable; | |||||
| import org.springframework.data.jpa.repository.JpaRepository; | |||||
| import org.springframework.data.repository.NoRepositoryBean; | |||||
| import com.ffii.core.entity.IdEntity; | |||||
| /** | |||||
| * @author Alex | |||||
| * @see https://docs.spring.io/spring-data/jpa/docs/2.7.0/reference/html/#jpa.query-methods.query-creation | |||||
| */ | |||||
| @NoRepositoryBean | |||||
| public interface AbstractRepository<T extends IdEntity<ID>, ID extends Serializable> extends JpaRepository<T, ID> { | |||||
| } | |||||
| @@ -0,0 +1,15 @@ | |||||
| package com.ffii.core.support; | |||||
| import org.apache.commons.logging.Log; | |||||
| import org.apache.commons.logging.LogFactory; | |||||
| /** @author Terence */ | |||||
| public abstract class AbstractService { | |||||
| protected final Log logger = LogFactory.getLog(getClass()); | |||||
| protected JdbcDao jdbcDao; | |||||
| public AbstractService(JdbcDao jdbcDao) { | |||||
| this.jdbcDao = jdbcDao; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,44 @@ | |||||
| package com.ffii.core.support; | |||||
| import java.util.UUID; | |||||
| import org.apache.commons.logging.Log; | |||||
| import org.apache.commons.logging.LogFactory; | |||||
| import org.springframework.http.HttpStatus; | |||||
| import org.springframework.http.ResponseEntity; | |||||
| import org.springframework.security.access.AccessDeniedException; | |||||
| import org.springframework.web.bind.annotation.ExceptionHandler; | |||||
| import org.springframework.web.bind.annotation.ResponseStatus; | |||||
| import org.springframework.web.bind.annotation.RestControllerAdvice; | |||||
| import org.springframework.web.server.ResponseStatusException; | |||||
| import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; | |||||
| import com.ffii.core.exception.ConflictException; | |||||
| import com.ffii.core.exception.InternalServerErrorException; | |||||
| import com.ffii.core.response.ErrorRes; | |||||
| import com.ffii.core.response.FailureRes; | |||||
| @RestControllerAdvice | |||||
| public class ErrorHandler extends ResponseEntityExceptionHandler { | |||||
| private final Log logger = LogFactory.getLog(getClass()); | |||||
| @ExceptionHandler({ ConflictException.class, ResponseStatusException.class }) | |||||
| public ResponseEntity<FailureRes> error409422(final Exception ex) { | |||||
| ResponseStatusException e = (ResponseStatusException) ex; | |||||
| return new ResponseEntity<>(new FailureRes(e.getReason()), e.getStatusCode()); | |||||
| } | |||||
| @ExceptionHandler(AccessDeniedException.class) | |||||
| public ResponseEntity<?> error403(final Exception ex) { | |||||
| return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); | |||||
| } | |||||
| @ExceptionHandler({ InternalServerErrorException.class, Exception.class }) | |||||
| @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) | |||||
| public ResponseEntity<ErrorRes> error500(final Exception ex) { | |||||
| UUID traceId = UUID.randomUUID(); | |||||
| logger.error("traceId: " + traceId, ex); | |||||
| return new ResponseEntity<>(new ErrorRes(traceId.toString()), HttpStatus.INTERNAL_SERVER_ERROR); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,437 @@ | |||||
| package com.ffii.core.support; | |||||
| import java.math.BigDecimal; | |||||
| import java.time.LocalDate; | |||||
| import java.time.LocalDateTime; | |||||
| import java.util.List; | |||||
| import java.util.Map; | |||||
| import java.util.Optional; | |||||
| import javax.sql.DataSource; | |||||
| import org.apache.commons.lang3.StringUtils; | |||||
| import org.springframework.dao.EmptyResultDataAccessException; | |||||
| import org.springframework.dao.IncorrectResultSizeDataAccessException; | |||||
| import org.springframework.dao.InvalidDataAccessApiUsageException; | |||||
| import org.springframework.jdbc.BadSqlGrammarException; | |||||
| import org.springframework.jdbc.IncorrectResultSetColumnCountException; | |||||
| import org.springframework.jdbc.core.BeanPropertyRowMapper; | |||||
| import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource; | |||||
| import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; | |||||
| import org.springframework.jdbc.core.namedparam.SqlParameterSourceUtils; | |||||
| /** @author Terence */ | |||||
| public class JdbcDao { | |||||
| private NamedParameterJdbcTemplate template; | |||||
| public JdbcDao(DataSource dataSource) { | |||||
| this.template = new NamedParameterJdbcTemplate(dataSource); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| * @throws IncorrectResultSizeDataAccessException: Incorrect result size | |||||
| */ | |||||
| public String queryForString(String sql) { | |||||
| return this.queryForString(sql, (Map<String, ?>) null); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| * @throws IncorrectResultSizeDataAccessException: Incorrect result size | |||||
| */ | |||||
| public String queryForString(String sql, Map<String, ?> paramMap) { | |||||
| try { | |||||
| return this.template.queryForObject(sql, paramMap, String.class); | |||||
| } catch (EmptyResultDataAccessException e) { | |||||
| return StringUtils.EMPTY; | |||||
| } | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| * @throws IncorrectResultSizeDataAccessException: Incorrect result size | |||||
| */ | |||||
| public String queryForString(String sql, Object paramObj) { | |||||
| try { | |||||
| return this.template.queryForObject(sql, new BeanPropertySqlParameterSource(paramObj), String.class); | |||||
| } catch (EmptyResultDataAccessException e) { | |||||
| return StringUtils.EMPTY; | |||||
| } | |||||
| } | |||||
| /** | |||||
| * @return {@code true} if non-zero, {@code false} if zero | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| * @throws IncorrectResultSizeDataAccessException: Incorrect result size | |||||
| */ | |||||
| public boolean queryForBoolean(String sql) { | |||||
| return this.queryForBoolean(sql, (Map<String, ?>) null); | |||||
| } | |||||
| /** | |||||
| * @return {@code true} if non-zero, {@code false} if zero | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| * @throws IncorrectResultSizeDataAccessException: Incorrect result size | |||||
| */ | |||||
| public boolean queryForBoolean(String sql, Map<String, ?> paramMap) { | |||||
| try { | |||||
| var rs = this.template.queryForObject(sql, paramMap, Boolean.class); | |||||
| return rs == null ? false : rs; | |||||
| } catch (EmptyResultDataAccessException e) { | |||||
| return false; | |||||
| } | |||||
| } | |||||
| /** | |||||
| * @return {@code true} if non-zero, {@code false} if zero | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| * @throws IncorrectResultSizeDataAccessException: Incorrect result size | |||||
| */ | |||||
| public boolean queryForBoolean(String sql, Object paramObj) { | |||||
| try { | |||||
| var rs = this.template.queryForObject(sql, new BeanPropertySqlParameterSource(paramObj), Boolean.class); | |||||
| return rs == null ? false : rs; | |||||
| } catch (EmptyResultDataAccessException e) { | |||||
| return false; | |||||
| } | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| * @throws IncorrectResultSizeDataAccessException: Incorrect result size | |||||
| */ | |||||
| public int queryForInt(String sql) { | |||||
| return this.queryForInt(sql, (Map<String, ?>) null); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| * @throws IncorrectResultSizeDataAccessException: Incorrect result size | |||||
| */ | |||||
| public int queryForInt(String sql, Map<String, ?> paramMap) { | |||||
| try { | |||||
| var rs = this.template.queryForObject(sql, paramMap, Integer.class); | |||||
| return rs == null ? 0 : rs; | |||||
| } catch (EmptyResultDataAccessException e) { | |||||
| return 0; | |||||
| } | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| * @throws IncorrectResultSizeDataAccessException: Incorrect result size | |||||
| */ | |||||
| public int queryForInt(String sql, Object paramObj) { | |||||
| try { | |||||
| var rs = this.template.queryForObject(sql, | |||||
| new BeanPropertySqlParameterSource(paramObj), Integer.class); | |||||
| return rs == null ? 0 : rs; | |||||
| } catch (EmptyResultDataAccessException e) { | |||||
| return 0; | |||||
| } | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| * @throws IncorrectResultSizeDataAccessException: Incorrect result size | |||||
| */ | |||||
| public BigDecimal queryForDecimal(String sql) { | |||||
| return this.queryForDecimal(sql, (Map<String, ?>) null); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| * @throws IncorrectResultSizeDataAccessException: Incorrect result size | |||||
| */ | |||||
| public BigDecimal queryForDecimal(String sql, Map<String, ?> paramMap) { | |||||
| try { | |||||
| return this.template.queryForObject(sql, paramMap, BigDecimal.class); | |||||
| } catch (EmptyResultDataAccessException e) { | |||||
| return BigDecimal.ZERO; | |||||
| } | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| * @throws IncorrectResultSizeDataAccessException: Incorrect result size | |||||
| */ | |||||
| public BigDecimal queryForDecimal(String sql, Object paramObj) { | |||||
| try { | |||||
| return this.template.queryForObject(sql, | |||||
| new BeanPropertySqlParameterSource(paramObj), BigDecimal.class); | |||||
| } catch (EmptyResultDataAccessException e) { | |||||
| return BigDecimal.ZERO; | |||||
| } | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws IncorrectResultSizeDataAccessException: Incorrect result size | |||||
| */ | |||||
| public <T> Optional<T> queryForEntity(String sql, Class<T> entity) { | |||||
| return this.queryForEntity(sql, (Map<String, ?>) null, entity); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| * @throws IncorrectResultSizeDataAccessException: Incorrect result size | |||||
| */ | |||||
| public <T> Optional<T> queryForEntity(String sql, Map<String, ?> paramMap, Class<T> entity) { | |||||
| try { | |||||
| return Optional.of(this.template.queryForObject(sql, paramMap, | |||||
| new BeanPropertyRowMapper<T>(entity))); | |||||
| } catch (EmptyResultDataAccessException e) { | |||||
| return Optional.empty(); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| * @throws IncorrectResultSizeDataAccessException: Incorrect result size | |||||
| */ | |||||
| public <T> Optional<T> queryForEntity(String sql, Object paramObj, Class<T> entity) { | |||||
| try { | |||||
| return Optional.of(this.template.queryForObject(sql, | |||||
| new BeanPropertySqlParameterSource(paramObj), new BeanPropertyRowMapper<T>(entity))); | |||||
| } catch (EmptyResultDataAccessException e) { | |||||
| return Optional.empty(); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| */ | |||||
| public <T> List<T> queryForList(String sql, Class<T> entity) { | |||||
| return this.queryForList(sql, (Map<String, ?>) null, entity); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| */ | |||||
| public <T> List<T> queryForList(String sql, Map<String, ?> paramMap, Class<T> entity) { | |||||
| return this.template.query(sql, paramMap, new BeanPropertyRowMapper<T>(entity)); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| */ | |||||
| public <T> List<T> queryForList(String sql, Object paramObj, Class<T> entity) { | |||||
| return this.template.query(sql, new BeanPropertySqlParameterSource(paramObj), | |||||
| new BeanPropertyRowMapper<T>(entity)); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| */ | |||||
| public List<Integer> queryForInts(String sql) { | |||||
| return this.queryForInts(sql, (Map<String, ?>) null); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| */ | |||||
| public List<Integer> queryForInts(String sql, Map<String, ?> paramMap) { | |||||
| return this.template.queryForList(sql, paramMap, Integer.class); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| */ | |||||
| public List<Integer> queryForInts(String sql, Object paramObj) { | |||||
| return this.template.queryForList(sql, new BeanPropertySqlParameterSource(paramObj), Integer.class); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| */ | |||||
| public List<LocalDate> queryForDates(String sql) { | |||||
| return this.queryForDates(sql, (Map<String, ?>) null); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| */ | |||||
| public List<LocalDate> queryForDates(String sql, Map<String, ?> paramMap) { | |||||
| return this.template.queryForList(sql, paramMap, LocalDate.class); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| */ | |||||
| public List<LocalDate> queryForDates(String sql, Object paramObj) { | |||||
| return this.template.queryForList(sql, new BeanPropertySqlParameterSource(paramObj), LocalDate.class); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| */ | |||||
| public List<LocalDateTime> queryForDatetimes(String sql) { | |||||
| return this.queryForDatetimes(sql, (Map<String, ?>) null); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| */ | |||||
| public List<LocalDateTime> queryForDatetimes(String sql, Map<String, ?> paramMap) { | |||||
| return this.template.queryForList(sql, paramMap, LocalDateTime.class); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| */ | |||||
| public List<LocalDateTime> queryForDatetimes(String sql, Object paramObj) { | |||||
| return this.template.queryForList(sql, new BeanPropertySqlParameterSource(paramObj), LocalDateTime.class); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| */ | |||||
| public List<String> queryForStrings(String sql) { | |||||
| return this.queryForStrings(sql, (Map<String, ?>) null); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| */ | |||||
| public List<String> queryForStrings(String sql, Map<String, ?> paramMap) { | |||||
| return this.template.queryForList(sql, paramMap, String.class); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| */ | |||||
| public List<String> queryForStrings(String sql, Object paramObj) { | |||||
| return this.template.queryForList(sql, new BeanPropertySqlParameterSource(paramObj), String.class); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| */ | |||||
| public List<Map<String, Object>> queryForList(String sql) { | |||||
| return this.queryForList(sql, (Map<String, ?>) null); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| */ | |||||
| public List<Map<String, Object>> queryForList(String sql, Map<String, ?> paramMap) { | |||||
| return this.template.queryForList(sql, paramMap); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| */ | |||||
| public List<Map<String, Object>> queryForList(String sql, Object paramObj) { | |||||
| return this.template.queryForList(sql, new BeanPropertySqlParameterSource(paramObj)); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| */ | |||||
| public Optional<Map<String, Object>> queryForMap(String sql) { | |||||
| return this.queryForMap(sql, (Map<String, ?>) null); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| */ | |||||
| public Optional<Map<String, Object>> queryForMap(String sql, Map<String, ?> paramMap) { | |||||
| try { | |||||
| return Optional.of(this.template.queryForMap(sql, paramMap)); | |||||
| } catch (EmptyResultDataAccessException e) { | |||||
| return Optional.empty(); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| */ | |||||
| public Optional<Map<String, Object>> queryForMap(String sql, Object paramObj) { | |||||
| try { | |||||
| return Optional.of(this.template.queryForMap(sql, new BeanPropertySqlParameterSource(paramObj))); | |||||
| } catch (EmptyResultDataAccessException e) { | |||||
| return Optional.empty(); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| */ | |||||
| public int executeUpdate(String sql) { | |||||
| return this.executeUpdate(sql, (Map<String, ?>) null); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| */ | |||||
| public int executeUpdate(String sql, Map<String, ?> paramMap) { | |||||
| return this.template.update(sql, paramMap); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| */ | |||||
| public int executeUpdate(String sql, Object paramObj) { | |||||
| return this.template.update(sql, new BeanPropertySqlParameterSource(paramObj)); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| */ | |||||
| public int[] batchUpdate(String sql, List<?> paramsMapOrObject) { | |||||
| return this.template.batchUpdate(sql, SqlParameterSourceUtils.createBatch(paramsMapOrObject)); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,85 @@ | |||||
| package com.ffii.core.utils; | |||||
| import java.io.UnsupportedEncodingException; | |||||
| import java.security.MessageDigest; | |||||
| import java.security.NoSuchAlgorithmException; | |||||
| import java.util.Arrays; | |||||
| import java.util.Base64; | |||||
| import javax.crypto.Cipher; | |||||
| import javax.crypto.spec.SecretKeySpec; | |||||
| import org.apache.commons.logging.Log; | |||||
| import org.apache.commons.logging.LogFactory; | |||||
| public class AES { | |||||
| protected final Log logger = LogFactory.getLog(getClass()); | |||||
| private static SecretKeySpec secretKey; | |||||
| private static byte[] key; | |||||
| public static void setKey(String myKey) { | |||||
| MessageDigest sha = null; | |||||
| try { | |||||
| key = myKey.getBytes("UTF-8"); | |||||
| sha = MessageDigest.getInstance("SHA-1"); | |||||
| key = sha.digest(key); | |||||
| key = Arrays.copyOf(key, 16); | |||||
| secretKey = new SecretKeySpec(key, "AES"); | |||||
| } catch (NoSuchAlgorithmException e) { | |||||
| e.printStackTrace(); | |||||
| } catch (UnsupportedEncodingException e) { | |||||
| e.printStackTrace(); | |||||
| } | |||||
| } | |||||
| public static String encrypt(String strToEncrypt, String secret) { | |||||
| try { | |||||
| setKey(secret); | |||||
| Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); | |||||
| cipher.init(Cipher.ENCRYPT_MODE, secretKey); | |||||
| return Base64.getEncoder().encodeToString(cipher.doFinal(strToEncrypt.getBytes("UTF-8"))); | |||||
| } catch (Exception e) { | |||||
| System.out.println("Error while encrypting: " + e.toString()); | |||||
| } | |||||
| return null; | |||||
| } | |||||
| public static String urlEncrypt(String strToEncrypt, String secret) { | |||||
| try { | |||||
| setKey(secret); | |||||
| Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); | |||||
| cipher.init(Cipher.ENCRYPT_MODE, secretKey); | |||||
| return Base64.getUrlEncoder().encodeToString(cipher.doFinal(strToEncrypt.getBytes("UTF-8"))); | |||||
| } catch (Exception e) { | |||||
| System.out.println("Error while encrypting: " + e.toString()); | |||||
| } | |||||
| return null; | |||||
| } | |||||
| public static String decrypt(String strToDecrypt, String secret) { | |||||
| try { | |||||
| setKey(secret); | |||||
| Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5PADDING"); | |||||
| cipher.init(Cipher.DECRYPT_MODE, secretKey); | |||||
| return new String(cipher.doFinal(Base64.getDecoder().decode(strToDecrypt)), "UTF-8"); | |||||
| } catch (Exception e) { | |||||
| System.out.println("Error while decrypting: " + e.toString()); | |||||
| } | |||||
| return null; | |||||
| } | |||||
| public static String urlDecrypt(String strToDecrypt, String secret) { | |||||
| try { | |||||
| setKey(secret); | |||||
| System.out.println("strToDecrypt: " + strToDecrypt); | |||||
| Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5PADDING"); | |||||
| cipher.init(Cipher.DECRYPT_MODE, secretKey); | |||||
| return new String(cipher.doFinal(Base64.getUrlDecoder().decode(strToDecrypt)), "UTF-8"); | |||||
| } catch (Exception e) { | |||||
| System.out.println("Error while decrypting: " + e.toString()); | |||||
| } | |||||
| return null; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,242 @@ | |||||
| package com.ffii.core.utils; | |||||
| import java.time.LocalDate; | |||||
| import java.time.LocalDateTime; | |||||
| import java.time.format.DateTimeFormatter; | |||||
| import java.time.format.DateTimeParseException; | |||||
| import java.util.ArrayList; | |||||
| import java.util.Arrays; | |||||
| import java.util.HashMap; | |||||
| import java.util.List; | |||||
| import java.util.Map; | |||||
| import jakarta.servlet.http.HttpServletRequest; | |||||
| import org.apache.commons.lang3.StringUtils; | |||||
| import org.springframework.web.bind.ServletRequestBindingException; | |||||
| import org.springframework.web.bind.ServletRequestUtils; | |||||
| /** @author Alex */ | |||||
| public class CriteriaArgsBuilder { | |||||
| private HttpServletRequest request; | |||||
| private Map<String, Object> args; | |||||
| private CriteriaArgsBuilder(HttpServletRequest request, Map<String, Object> args) { | |||||
| this.args = args; | |||||
| this.request = request; | |||||
| } | |||||
| public static CriteriaArgsBuilder withRequest(HttpServletRequest request) { | |||||
| return new CriteriaArgsBuilder(request, new HashMap<String, Object>()); | |||||
| } | |||||
| public static CriteriaArgsBuilder withRequestNMap(HttpServletRequest request, Map<String, Object> args) { | |||||
| return new CriteriaArgsBuilder(request, args); | |||||
| } | |||||
| public CriteriaArgsBuilder addStringExact(String paramName) throws ServletRequestBindingException { | |||||
| String value = StringUtils.trimToNull(ServletRequestUtils.getStringParameter(this.request, paramName)); | |||||
| if (value != null) | |||||
| args.put(paramName, value); | |||||
| return this; | |||||
| } | |||||
| public CriteriaArgsBuilder addStringLike(String paramName) throws ServletRequestBindingException { | |||||
| String value = StringUtils.trimToNull(ServletRequestUtils.getStringParameter(this.request, paramName)); | |||||
| if (value != null) | |||||
| args.put(paramName, "%" + value + "%"); | |||||
| return this; | |||||
| } | |||||
| public CriteriaArgsBuilder addString(String paramName) throws ServletRequestBindingException { | |||||
| return this.addStringExact(paramName); | |||||
| } | |||||
| public CriteriaArgsBuilder addStringStartsWith(String paramName) throws ServletRequestBindingException { | |||||
| String value = StringUtils.trimToNull(ServletRequestUtils.getStringParameter(this.request, paramName)); | |||||
| if (value != null) | |||||
| args.put(paramName, value + "%"); | |||||
| return this; | |||||
| } | |||||
| public CriteriaArgsBuilder addStringEndsWith(String paramName) throws ServletRequestBindingException { | |||||
| String value = StringUtils.trimToNull(ServletRequestUtils.getStringParameter(this.request, paramName)); | |||||
| if (value != null) | |||||
| args.put(paramName, "%" + value); | |||||
| return this; | |||||
| } | |||||
| public CriteriaArgsBuilder addStringList(String paramName) throws ServletRequestBindingException { | |||||
| String[] params = ServletRequestUtils.getStringParameters(this.request, paramName); | |||||
| if (params.length > 0) { | |||||
| List<String> value = new ArrayList<String>(params.length); | |||||
| for (String param : params) | |||||
| if (StringUtils.isNotBlank(param)) | |||||
| value.add(param); | |||||
| if (value.size() > 0) | |||||
| args.put(paramName, value); | |||||
| } | |||||
| return this; | |||||
| } | |||||
| public CriteriaArgsBuilder addStringCsv(String paramName) throws ServletRequestBindingException { | |||||
| String text = ServletRequestUtils.getStringParameter(this.request, paramName); | |||||
| if (text != null && StringUtils.isNotEmpty(text)) | |||||
| args.put(paramName, Arrays.asList(text.split(","))); | |||||
| return this; | |||||
| } | |||||
| public CriteriaArgsBuilder addInteger(String paramName) throws ServletRequestBindingException { | |||||
| Integer value = StringUtils.isNotBlank(this.request.getParameter(paramName)) | |||||
| ? ServletRequestUtils.getRequiredIntParameter(request, paramName) | |||||
| : null; | |||||
| if (value != null) | |||||
| args.put(paramName, value); | |||||
| return this; | |||||
| } | |||||
| public CriteriaArgsBuilder addNonZeroInteger(String paramName) throws ServletRequestBindingException { | |||||
| Integer value = StringUtils.isNotBlank(this.request.getParameter(paramName)) | |||||
| ? ServletRequestUtils.getRequiredIntParameter(request, paramName) | |||||
| : null; | |||||
| if (value != null && value.intValue() != 0) | |||||
| args.put(paramName, value); | |||||
| return this; | |||||
| } | |||||
| public CriteriaArgsBuilder addIntegerList(String paramName) throws ServletRequestBindingException { | |||||
| int[] params = ServletRequestUtils.getIntParameters(request, paramName); | |||||
| if (params.length > 0) { | |||||
| List<Integer> values = new ArrayList<Integer>(); | |||||
| for (int param : params) | |||||
| values.add(param); | |||||
| args.put(paramName, values); | |||||
| } | |||||
| return this; | |||||
| } | |||||
| public CriteriaArgsBuilder addNonZeroIntegerList(String paramName) throws ServletRequestBindingException { | |||||
| int[] params = ServletRequestUtils.getIntParameters(request, paramName); | |||||
| if (params.length > 0) { | |||||
| List<Integer> values = new ArrayList<Integer>(); | |||||
| for (int param : params) | |||||
| if (param != 0) | |||||
| values.add(param); | |||||
| args.put(paramName, values); | |||||
| } | |||||
| return this; | |||||
| } | |||||
| public CriteriaArgsBuilder addLong(String paramName) throws ServletRequestBindingException { | |||||
| Long value = StringUtils.isNotBlank(this.request.getParameter(paramName)) | |||||
| ? ServletRequestUtils.getRequiredLongParameter(request, paramName) | |||||
| : null; | |||||
| if (value != null) | |||||
| args.put(paramName, value); | |||||
| return this; | |||||
| } | |||||
| public CriteriaArgsBuilder addNonZeroLong(String paramName) throws ServletRequestBindingException { | |||||
| Long value = StringUtils.isNotBlank(this.request.getParameter(paramName)) | |||||
| ? ServletRequestUtils.getRequiredLongParameter(request, paramName) | |||||
| : null; | |||||
| if (value != null && value.longValue() != 0L) | |||||
| args.put(paramName, value); | |||||
| return this; | |||||
| } | |||||
| public CriteriaArgsBuilder addDatetime(String paramName) throws ServletRequestBindingException { | |||||
| String value = ServletRequestUtils.getStringParameter(request, paramName); | |||||
| if (StringUtils.isNotBlank(value)) { | |||||
| try { | |||||
| args.put(paramName, LocalDateTime.parse(value)); | |||||
| } catch (DateTimeParseException e) { | |||||
| throw new ServletRequestBindingException(paramName); | |||||
| } | |||||
| } | |||||
| return this; | |||||
| } | |||||
| public CriteriaArgsBuilder addDatetime(String paramName, DateTimeFormatter formatter) | |||||
| throws ServletRequestBindingException { | |||||
| String value = ServletRequestUtils.getStringParameter(request, paramName); | |||||
| if (StringUtils.isNotBlank(value)) { | |||||
| try { | |||||
| args.put(paramName, LocalDateTime.parse(value, formatter)); | |||||
| } catch (DateTimeParseException e) { | |||||
| throw new ServletRequestBindingException(paramName); | |||||
| } | |||||
| } | |||||
| return this; | |||||
| } | |||||
| public CriteriaArgsBuilder addDate(String paramName) throws ServletRequestBindingException { | |||||
| String value = ServletRequestUtils.getStringParameter(request, paramName); | |||||
| if (StringUtils.isNotBlank(value)) { | |||||
| try { | |||||
| args.put(paramName, LocalDate.parse(value)); | |||||
| } catch (DateTimeParseException e) { | |||||
| throw new ServletRequestBindingException(paramName); | |||||
| } | |||||
| } | |||||
| return this; | |||||
| } | |||||
| public CriteriaArgsBuilder addDate(String paramName, DateTimeFormatter formatter) | |||||
| throws ServletRequestBindingException { | |||||
| String value = ServletRequestUtils.getStringParameter(request, paramName); | |||||
| if (StringUtils.isNotBlank(value)) { | |||||
| try { | |||||
| args.put(paramName, LocalDate.parse(value, formatter)); | |||||
| } catch (DateTimeParseException e) { | |||||
| throw new ServletRequestBindingException(paramName); | |||||
| } | |||||
| } | |||||
| return this; | |||||
| } | |||||
| public CriteriaArgsBuilder addDateTo(String paramName) throws ServletRequestBindingException { | |||||
| String value = ServletRequestUtils.getStringParameter(request, paramName); | |||||
| if (StringUtils.isNotBlank(value)) { | |||||
| try { | |||||
| args.put(paramName, LocalDate.parse(value).plusDays(1)); | |||||
| } catch (DateTimeParseException e) { | |||||
| throw new ServletRequestBindingException(paramName); | |||||
| } | |||||
| } | |||||
| return this; | |||||
| } | |||||
| public CriteriaArgsBuilder addDateTo(String paramName, DateTimeFormatter formatter) | |||||
| throws ServletRequestBindingException { | |||||
| String value = ServletRequestUtils.getStringParameter(request, paramName); | |||||
| if (StringUtils.isNotBlank(value)) { | |||||
| try { | |||||
| args.put(paramName, LocalDate.parse(value, formatter).plusDays(1)); | |||||
| } catch (DateTimeParseException e) { | |||||
| throw new ServletRequestBindingException(paramName); | |||||
| } | |||||
| } | |||||
| return this; | |||||
| } | |||||
| public CriteriaArgsBuilder addBoolean(String paramName) throws ServletRequestBindingException { | |||||
| if (request.getParameter(paramName) == null || request.getParameter(paramName).isEmpty()) { | |||||
| return this; | |||||
| } | |||||
| Boolean value = ServletRequestUtils.getBooleanParameter(request, paramName); | |||||
| args.put(paramName, value); | |||||
| return this; | |||||
| } | |||||
| public CriteriaArgsBuilder put(String key, Object value) { | |||||
| args.put(key, value); | |||||
| return this; | |||||
| } | |||||
| public Map<String, Object> build() { | |||||
| return this.args; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,778 @@ | |||||
| package com.ffii.core.utils; | |||||
| import java.io.ByteArrayInputStream; | |||||
| import java.io.ByteArrayOutputStream; | |||||
| import java.io.IOException; | |||||
| import java.io.InputStream; | |||||
| import java.io.OutputStream; | |||||
| import java.math.BigDecimal; | |||||
| import java.math.RoundingMode; | |||||
| import java.time.LocalDate; | |||||
| import java.time.LocalDateTime; | |||||
| import java.time.LocalTime; | |||||
| import java.time.ZoneId; | |||||
| import java.time.format.DateTimeFormatter; | |||||
| import java.time.format.DateTimeParseException; | |||||
| import java.util.Calendar; | |||||
| import java.util.Date; | |||||
| import java.util.HashMap; | |||||
| import java.util.Map; | |||||
| import org.apache.commons.lang3.StringUtils; | |||||
| import org.apache.commons.lang3.math.NumberUtils; | |||||
| import org.apache.poi.openxml4j.exceptions.InvalidFormatException; | |||||
| import org.apache.poi.openxml4j.opc.OPCPackage; | |||||
| import org.apache.poi.poifs.crypt.EncryptionInfo; | |||||
| import org.apache.poi.poifs.crypt.EncryptionMode; | |||||
| import org.apache.poi.poifs.crypt.Encryptor; | |||||
| import org.apache.poi.poifs.filesystem.POIFSFileSystem; | |||||
| import org.apache.poi.ss.usermodel.Cell; | |||||
| import org.apache.poi.ss.usermodel.CellType; | |||||
| import org.apache.poi.ss.usermodel.DataFormatter; | |||||
| import org.apache.poi.ss.usermodel.DateUtil; | |||||
| import org.apache.poi.ss.usermodel.RichTextString; | |||||
| import org.apache.poi.ss.usermodel.Row; | |||||
| import org.apache.poi.ss.usermodel.Sheet; | |||||
| import org.apache.poi.ss.usermodel.Workbook; | |||||
| import org.apache.poi.ss.util.CellRangeAddress; | |||||
| import org.apache.poi.xssf.usermodel.XSSFWorkbook; | |||||
| import org.springframework.core.io.ClassPathResource; | |||||
| import org.springframework.core.io.Resource; | |||||
| import org.springframework.core.io.ResourceLoader; | |||||
| import jakarta.servlet.http.HttpServletResponse; | |||||
| public abstract class ExcelUtils { | |||||
| /** | |||||
| * static A to Z char array | |||||
| */ | |||||
| private static final char[] A2Z = { | |||||
| 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', | |||||
| 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', | |||||
| 'U', 'V', 'W', 'X', 'Y', 'Z' }; | |||||
| private static final DataFormatter DATA_FORMATTER = new DataFormatter(); | |||||
| /** max rows limit of .xls **/ | |||||
| public static final int MAX_ROWS = 65536; | |||||
| /** max columns limit of .xls **/ | |||||
| public static final int MAX_COLS = 256; | |||||
| /** | |||||
| * Column reference to index (0-based) map, support up to 256 columns (compatible with .xls format) | |||||
| */ | |||||
| public static final Map<String, Integer> COL_IDX = new HashMap<String, Integer>(MAX_COLS, 1.0f); | |||||
| static { | |||||
| for (int columnIndex = 0; columnIndex < MAX_COLS; columnIndex++) { | |||||
| int tempColumnCount = columnIndex; | |||||
| StringBuilder sb = new StringBuilder(2); | |||||
| do { | |||||
| sb.insert(0, A2Z[tempColumnCount % 26]); | |||||
| tempColumnCount = (tempColumnCount / 26) - 1; | |||||
| } while (tempColumnCount >= 0); | |||||
| COL_IDX.put(sb.toString(), Integer.valueOf(columnIndex)); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Load XSSF workbook (xlsx file) from template source. | |||||
| * | |||||
| * @param url | |||||
| * the relative path to the template source, e.g. "WEB-INF/excel/exampleReportTemplate.xlsx" | |||||
| * | |||||
| * @return the workbook, or null if the template file cannot be loaded | |||||
| */ | |||||
| public static final Workbook loadXSSFWorkbookFromTemplateSource(ResourceLoader resourceLoader, String url) { | |||||
| Resource resource = resourceLoader.getResource(url); | |||||
| try { | |||||
| return new XSSFWorkbook(resource.getInputStream()); | |||||
| } catch (IOException e) { | |||||
| return null; | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Write the workbook to byte array. | |||||
| * | |||||
| * @param workbook | |||||
| * The Excel workbook (cannot be null) | |||||
| * | |||||
| * @return the byte[], or null if IO exception occurred | |||||
| */ | |||||
| public static final byte[] toByteArray(Workbook workbook) { | |||||
| ByteArrayOutputStream baos = new ByteArrayOutputStream(); | |||||
| try { | |||||
| workbook.write(baos); | |||||
| } catch (IOException e) { | |||||
| return null; | |||||
| } | |||||
| return baos.toByteArray(); | |||||
| } | |||||
| /** | |||||
| * Check if the cell exists in the given sheet, row and column. | |||||
| * | |||||
| * @param sheet | |||||
| * the Sheet (cannot be null) | |||||
| * @param rowIndex | |||||
| * 0-based row index | |||||
| * @param colIndex | |||||
| * 0-based column index | |||||
| * | |||||
| * @return {@code true} if cell exists, else {@code false} | |||||
| */ | |||||
| public static final boolean isCellExists(Sheet sheet, int rowIndex, int colIndex) { | |||||
| Row row = sheet.getRow(rowIndex); | |||||
| if (row != null) { | |||||
| Cell cell = row.getCell(colIndex); | |||||
| return cell != null; | |||||
| } | |||||
| return false; | |||||
| } | |||||
| /** | |||||
| * Convenient method to obtain the cell in the given sheet, row and column. | |||||
| * <p> | |||||
| * Creates the row and the cell if not already exist. | |||||
| * | |||||
| * @param sheet | |||||
| * the Sheet (cannot be null) | |||||
| * @param rowIndex | |||||
| * 0-based row index | |||||
| * @param colIndex | |||||
| * 0-based column index | |||||
| * | |||||
| * @return the Cell (never null) | |||||
| */ | |||||
| public static final Cell getCell(Sheet sheet, int rowIndex, int colIndex) { | |||||
| Row row = sheet.getRow(rowIndex); | |||||
| if (row == null) { | |||||
| row = sheet.createRow(rowIndex); | |||||
| } | |||||
| Cell cell = row.getCell(colIndex); | |||||
| if (cell == null) { | |||||
| cell = row.createCell(colIndex); | |||||
| } | |||||
| return cell; | |||||
| } | |||||
| /** | |||||
| * Get column index by column reference (support up to 256 columns) | |||||
| * | |||||
| * @param columnRef | |||||
| * column reference such as "A", "B", "AA", "AB"... | |||||
| * | |||||
| * @return the column index | |||||
| * | |||||
| * @throws NullPointerException | |||||
| * if column reference is invalid or the index exceeds 256 | |||||
| */ | |||||
| public static final int getColumnIndex(String columnRef) { | |||||
| return COL_IDX.get(columnRef); | |||||
| } | |||||
| /** | |||||
| * Get column reference by column index | |||||
| * | |||||
| * @param columnIndex | |||||
| * 0-based column index | |||||
| * | |||||
| * @return the column reference such as "A", "B", "AA", "AB"... | |||||
| */ | |||||
| public static final String getColumnRef(int columnIndex) { | |||||
| StringBuilder sb = new StringBuilder(); | |||||
| int tempColumnCount = columnIndex; | |||||
| do { | |||||
| sb.insert(0, A2Z[tempColumnCount % 26]); | |||||
| tempColumnCount = (tempColumnCount / 26) - 1; | |||||
| } while (tempColumnCount >= 0); | |||||
| return sb.toString(); | |||||
| } | |||||
| /** | |||||
| * Get the Excel Cell Ref String by columnIndex and rowIndex | |||||
| * | |||||
| * @param columnIndex | |||||
| * 0-based column index | |||||
| * @param rowIndex | |||||
| * 0-based row index | |||||
| */ | |||||
| public static final String getCellRefString(int columnIndex, int rowIndex) { | |||||
| StringBuilder sb = new StringBuilder(); | |||||
| int tempColumnCount = columnIndex; | |||||
| do { | |||||
| sb.insert(0, A2Z[tempColumnCount % 26]); | |||||
| tempColumnCount = (tempColumnCount / 26) - 1; | |||||
| } while (tempColumnCount >= 0); | |||||
| sb.append(rowIndex + 1); | |||||
| return sb.toString(); | |||||
| } | |||||
| /** | |||||
| * Get Cell value as <code>String</code> | |||||
| */ | |||||
| public static String getStringValue(Cell cell) { | |||||
| if (cell != null && cell.getCellType() == CellType.FORMULA) { | |||||
| try { | |||||
| return cell.getStringCellValue(); | |||||
| } catch (Exception e) { | |||||
| return ""; | |||||
| } | |||||
| } | |||||
| return DATA_FORMATTER.formatCellValue(cell); | |||||
| } | |||||
| /** | |||||
| * Get Cell value as <code>BigDecimal</code>, with a fallback value | |||||
| * <p> | |||||
| * Only support {@link CellType#NUMERIC} and {@link CellType#STRING} | |||||
| * | |||||
| * @return the <code>BigDecimal</code> value, or the default value if cell is <code>null</code> or cell type is {@link CellType#BLANK} | |||||
| */ | |||||
| public static BigDecimal getDecimalValue(Cell cell, BigDecimal defaultValue) { | |||||
| if (cell == null || cell.getCellType() == CellType.BLANK) return defaultValue; | |||||
| if (cell.getCellType() == CellType.STRING) { | |||||
| return new BigDecimal(cell.getStringCellValue()); | |||||
| } else { | |||||
| return BigDecimal.valueOf(cell.getNumericCellValue()); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Get Cell value as <code>BigDecimal</code> | |||||
| * <p> | |||||
| * Only support {@link CellType#NUMERIC} and {@link CellType#STRING} | |||||
| * | |||||
| * @return the <code>BigDecimal</code> value, or <code>BigDecimal.ZERO</code> if cell is <code>null</code> or cell type is {@link CellType#BLANK} | |||||
| */ | |||||
| public static BigDecimal getDecimalValue(Cell cell) { | |||||
| return getDecimalValue(cell, BigDecimal.ZERO); | |||||
| } | |||||
| /** | |||||
| * Get Cell value as <code>double</code> | |||||
| * <p> | |||||
| * Only support {@link CellType#NUMERIC} and {@link CellType#STRING} | |||||
| */ | |||||
| public static double getDoubleValue(Cell cell) { | |||||
| if (cell == null) return 0.0; | |||||
| if (cell.getCellType() == CellType.STRING) { | |||||
| return NumberUtils.toDouble(cell.getStringCellValue()); | |||||
| } else { | |||||
| return cell.getNumericCellValue(); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Get Cell value as <code>int</code> (rounded half-up to the nearest integer) | |||||
| * <p> | |||||
| * Only support {@link CellType#NUMERIC} and {@link CellType#STRING} | |||||
| */ | |||||
| public static int getIntValue(Cell cell) { | |||||
| return BigDecimal.valueOf(getDoubleValue(cell)).setScale(0, RoundingMode.HALF_UP).intValue(); | |||||
| } | |||||
| /** | |||||
| * Get Cell Integer value (truncated) | |||||
| */ | |||||
| public static Integer getIntValue(Cell cell, Integer defaultValue) { | |||||
| if (cell == null) return defaultValue; | |||||
| if (cell.getCellType() == CellType.STRING) { | |||||
| return NumberUtils.toInt(cell.getStringCellValue(), defaultValue); | |||||
| } else { | |||||
| return (int) cell.getNumericCellValue(); | |||||
| } | |||||
| } | |||||
| public static LocalDate getDateValue(Cell cell, DateTimeFormatter formatter) { | |||||
| if (cell == null) return null; | |||||
| if (cell.getCellType() == CellType.STRING) { | |||||
| try { | |||||
| return LocalDate.parse(cell.getStringCellValue(), formatter); | |||||
| } catch (DateTimeParseException e) { | |||||
| return null; | |||||
| } | |||||
| } | |||||
| if (DateUtil.isCellDateFormatted(cell)) { | |||||
| try { | |||||
| return DateUtil.getJavaDate(cell.getNumericCellValue()).toInstant() | |||||
| .atZone(ZoneId.systemDefault()) | |||||
| .toLocalDate(); | |||||
| } catch (NumberFormatException e) { | |||||
| return null; | |||||
| } | |||||
| } else { | |||||
| return null; | |||||
| } | |||||
| } | |||||
| public static LocalDateTime getDatetimeValue(Cell cell, DateTimeFormatter formatter) { | |||||
| if (cell == null) return null; | |||||
| if (cell.getCellType() == CellType.STRING) { | |||||
| try { | |||||
| return LocalDateTime.parse(cell.getStringCellValue(), formatter); | |||||
| } catch (DateTimeParseException e) { | |||||
| return null; | |||||
| } | |||||
| } | |||||
| if (DateUtil.isCellDateFormatted(cell)) { | |||||
| try { | |||||
| return DateUtil.getJavaDate(cell.getNumericCellValue()).toInstant() | |||||
| .atZone(ZoneId.systemDefault()) | |||||
| .toLocalDateTime(); | |||||
| } catch (NumberFormatException e) { | |||||
| return null; | |||||
| } | |||||
| } else { | |||||
| return null; | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Convenient method to set Cell value | |||||
| * | |||||
| * @param cell | |||||
| * the Cell (cannot be null) | |||||
| * @param value | |||||
| * the value to set | |||||
| */ | |||||
| public static void setCellValue(Cell cell, Object value) { | |||||
| if (value instanceof String) | |||||
| cell.setCellValue((String) value); | |||||
| else if (value instanceof RichTextString) | |||||
| cell.setCellValue((RichTextString) value); | |||||
| else if (value instanceof Number) | |||||
| cell.setCellValue(((Number) value).doubleValue()); | |||||
| else if (value instanceof Boolean) | |||||
| cell.setCellValue(((Boolean) value).booleanValue()); | |||||
| else if (value instanceof Calendar) | |||||
| cell.setCellValue((Calendar) value); | |||||
| else if (value instanceof Date) | |||||
| cell.setCellValue((Date) value); | |||||
| else if (value instanceof LocalDate) | |||||
| cell.setCellValue((LocalDate) value); | |||||
| else if (value instanceof LocalTime) | |||||
| cell.setCellValue(((LocalTime) value).toString()); | |||||
| else if (value instanceof LocalDateTime) | |||||
| cell.setCellValue((LocalDateTime) value); | |||||
| else if (value == null) | |||||
| cell.setCellValue(""); | |||||
| else | |||||
| throw new IllegalArgumentException(value.getClass().toString() + " is not supported"); | |||||
| } | |||||
| /** | |||||
| * Convenient method to set Cell value by Sheet, row index, and column index | |||||
| * | |||||
| * @param sheet | |||||
| * the Sheet (cannot be null) | |||||
| * @param rowIndex | |||||
| * 0-based row index | |||||
| * @param colIndex | |||||
| * 0-based column index | |||||
| * @param value | |||||
| * the value to set | |||||
| */ | |||||
| public static void setCellValue(Sheet sheet, int rowIndex, int colIndex, Object value) { | |||||
| setCellValue(getCell(sheet, rowIndex, colIndex), value); | |||||
| } | |||||
| /** | |||||
| * Increase Row Height (if necessary, but never decrease it) by counting the no. of lines in a String value | |||||
| * | |||||
| * @param sheet | |||||
| * The Excel worksheet | |||||
| * @param row | |||||
| * The row index (0-based) | |||||
| * @param value | |||||
| * The (multi-line) String value to count for the no. of lines | |||||
| * @param heightInPoints | |||||
| * The height (in points) for 1 line of text | |||||
| */ | |||||
| public static void increaseRowHeight(Sheet sheet, int row, String value, int heightInPoints) { | |||||
| int lines = StringUtils.countMatches(value, "\n") + 1; // count no. of lines | |||||
| float newHeight = heightInPoints * lines; | |||||
| Row r = sheet.getRow(row); | |||||
| if (r == null) r = sheet.createRow(row); | |||||
| // increase the row height if necessary, but never decrease it | |||||
| if (r.getHeightInPoints() < newHeight) { | |||||
| r.setHeightInPoints(newHeight); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Add merged region (i.e. merge cells) | |||||
| * | |||||
| * @param sheet | |||||
| * The Excel worksheet | |||||
| * @param firstRowIdx | |||||
| * The first row index (0-based) | |||||
| * @param lastRowIdx | |||||
| * The last row index (0-based) | |||||
| * @param firstColIdx | |||||
| * The first column index (0-based) | |||||
| * @param lastColIdx | |||||
| * The last column index (0-based) | |||||
| */ | |||||
| public static void addMergedRegion(Sheet sheet, int firstRowIdx, int lastRowIdx, int firstColIdx, int lastColIdx) { | |||||
| CellRangeAddress cellRangeAddress = new CellRangeAddress(firstRowIdx, lastRowIdx, firstColIdx, lastColIdx); | |||||
| sheet.addMergedRegion(cellRangeAddress); | |||||
| } | |||||
| /** | |||||
| * Copy and Insert Row | |||||
| * | |||||
| * @param workbook | |||||
| * The Excel workbook | |||||
| * @param sourceSheet | |||||
| * The source Excel worksheet | |||||
| * @param destinationSheet | |||||
| * The destination Excel worksheet | |||||
| * @param sourceRowNum | |||||
| * The source row index (0-based) to copy from | |||||
| * @param destinationRowNum | |||||
| * The destination row index (0-based) to insert into (from the copied row) | |||||
| */ | |||||
| public static void copyAndInsertRow(Workbook workbook, Sheet sourceSheet, Sheet destinationSheet, int sourceRowNum, int destinationRowNum) { | |||||
| // get the source / destination row | |||||
| Row sourceRow = sourceSheet.getRow(sourceRowNum); | |||||
| Row destRow = destinationSheet.getRow(destinationRowNum); | |||||
| // if the row exist in destination, push down all rows by 1 | |||||
| if (destRow != null) { | |||||
| destinationSheet.shiftRows(destinationRowNum, destinationSheet.getLastRowNum(), 1, true, false); | |||||
| } | |||||
| // create a new row | |||||
| destRow = destinationSheet.createRow(destinationRowNum); | |||||
| // loop through source columns to add to new row | |||||
| for (int i = 0; i < sourceRow.getLastCellNum(); i++) { | |||||
| // grab a copy of the old cell | |||||
| Cell oldCell = sourceRow.getCell(i); | |||||
| // if the old cell is null jump to next cell | |||||
| if (oldCell == null) continue; | |||||
| // create a new cell in destination row | |||||
| Cell newCell = destRow.createCell(i); | |||||
| // apply cell style to new cell from old cell | |||||
| newCell.setCellStyle(oldCell.getCellStyle()); | |||||
| // if there is a cell comment, copy | |||||
| if (oldCell.getCellComment() != null) { | |||||
| newCell.setCellComment(oldCell.getCellComment()); | |||||
| } | |||||
| // if there is a cell hyperlink, copy | |||||
| if (oldCell.getHyperlink() != null) { | |||||
| newCell.setHyperlink(oldCell.getHyperlink()); | |||||
| } | |||||
| // copy the cell data value | |||||
| switch (oldCell.getCellType()) { | |||||
| case NUMERIC: | |||||
| newCell.setCellValue(oldCell.getNumericCellValue()); | |||||
| break; | |||||
| case STRING: | |||||
| newCell.setCellValue(oldCell.getRichStringCellValue()); | |||||
| break; | |||||
| case FORMULA: | |||||
| newCell.setCellFormula(oldCell.getCellFormula()); | |||||
| break; | |||||
| case BLANK: | |||||
| newCell.setCellValue(oldCell.getStringCellValue()); | |||||
| break; | |||||
| case BOOLEAN: | |||||
| newCell.setCellValue(oldCell.getBooleanCellValue()); | |||||
| break; | |||||
| case ERROR: | |||||
| newCell.setCellErrorValue(oldCell.getErrorCellValue()); | |||||
| break; | |||||
| default: | |||||
| break; | |||||
| } | |||||
| } | |||||
| // if there are any merged regions in the source row, copy to new row | |||||
| for (int i = 0; i < sourceSheet.getNumMergedRegions(); i++) { | |||||
| CellRangeAddress cellRangeAddress = sourceSheet.getMergedRegion(i); | |||||
| if (cellRangeAddress.getFirstRow() == sourceRow.getRowNum()) { | |||||
| addMergedRegion( | |||||
| destinationSheet, | |||||
| destRow.getRowNum(), | |||||
| (destRow.getRowNum() + (cellRangeAddress.getLastRow() - cellRangeAddress.getFirstRow())), | |||||
| cellRangeAddress.getFirstColumn(), | |||||
| cellRangeAddress.getLastColumn()); | |||||
| } | |||||
| } | |||||
| // copy row height | |||||
| destRow.setHeight(sourceRow.getHeight()); | |||||
| } | |||||
| /** | |||||
| * Copy and Insert Row | |||||
| * | |||||
| * @param workbook | |||||
| * The Excel workbook | |||||
| * @param sheet | |||||
| * The Excel worksheet | |||||
| * @param sourceRowNum | |||||
| * The source row index (0-based) to copy from | |||||
| * @param destinationRowNum | |||||
| * The destination row index (0-based) to insert into (from the copied row) | |||||
| */ | |||||
| public static void copyAndInsertRow(Workbook workbook, Sheet sheet, int sourceRowNum, int destinationRowNum) { | |||||
| copyAndInsertRow(workbook, sheet, sheet, sourceRowNum, destinationRowNum); | |||||
| } | |||||
| public static void copyAndInsertRow(Workbook workbook, Sheet sourceSheet, int sourceRowNum, int destinationRowNum, int times) { | |||||
| // get the source / destination row | |||||
| Row sourceRow = sourceSheet.getRow(sourceRowNum); | |||||
| Row[] destRows = new Row[times]; | |||||
| for (int j = 0; j < times; j++) { | |||||
| Row destRow = sourceSheet.getRow(destinationRowNum + j); | |||||
| // if the row exist in destination, push down all rows by 1 | |||||
| if (destRow != null) { | |||||
| sourceSheet.shiftRows(destinationRowNum + j, sourceSheet.getLastRowNum(), 1, true, false); | |||||
| } | |||||
| // create a new row | |||||
| destRows[j] = sourceSheet.createRow(destinationRowNum + j); | |||||
| // copy row height | |||||
| destRows[j].setHeight(sourceRow.getHeight()); | |||||
| } | |||||
| // loop through source columns to add to new row | |||||
| for (int i = 0; i < sourceRow.getLastCellNum(); i++) { | |||||
| // grab a copy of the old cell | |||||
| Cell oldCell = sourceRow.getCell(i); | |||||
| // if the old cell is null jump to next cell | |||||
| if (oldCell == null) continue; | |||||
| for (int k = 0; k < times; k++) { | |||||
| // create a new cell in destination row | |||||
| Cell newCell = destRows[k].createCell(i); | |||||
| // apply cell style to new cell from old cell | |||||
| newCell.setCellStyle(oldCell.getCellStyle()); | |||||
| } | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Copy Column | |||||
| * | |||||
| * @param workbook | |||||
| * The Excel workbook | |||||
| * @param sourceSheet | |||||
| * The source Excel worksheet | |||||
| * @param destinationSheet | |||||
| * The destination Excel worksheet | |||||
| * @param rowStart | |||||
| * The source row start index (0-based) to copy from | |||||
| * @param rowEnd | |||||
| * The source row end index (0-based) to copy from | |||||
| * @param sourceColumnNum | |||||
| * The source column index (0-based) to copy from | |||||
| * @param destinationColumnNum | |||||
| * The destination column index (0-based) to copy into (from the copied row) | |||||
| */ | |||||
| public static void copyColumn(Workbook workbook, Sheet sourceSheet, Sheet destinationSheet, int rowStart, int rowEnd, int sourceColumnNum, | |||||
| int destinationColumnNum) { | |||||
| for (int i = rowStart; i <= rowEnd; i++) { | |||||
| Row sourceRow = sourceSheet.getRow(i); | |||||
| if (sourceRow == null) continue; | |||||
| Row destinationRow = destinationSheet.getRow(i); | |||||
| if (destinationRow == null) destinationRow = destinationSheet.createRow(i); | |||||
| Cell oldCell = sourceRow.getCell(sourceColumnNum); | |||||
| if (oldCell == null) continue; | |||||
| Cell newCell = destinationRow.createCell(destinationColumnNum); | |||||
| newCell.setCellStyle(oldCell.getCellStyle()); | |||||
| if (oldCell.getCellComment() != null) { | |||||
| newCell.setCellComment(oldCell.getCellComment()); | |||||
| } | |||||
| if (oldCell.getHyperlink() != null) { | |||||
| newCell.setHyperlink(oldCell.getHyperlink()); | |||||
| } | |||||
| switch (oldCell.getCellType()) { | |||||
| case NUMERIC: | |||||
| newCell.setCellValue(oldCell.getNumericCellValue()); | |||||
| break; | |||||
| case STRING: | |||||
| newCell.setCellValue(oldCell.getRichStringCellValue()); | |||||
| break; | |||||
| case FORMULA: | |||||
| newCell.setCellFormula(oldCell.getCellFormula()); | |||||
| break; | |||||
| case BLANK: | |||||
| newCell.setCellValue(oldCell.getStringCellValue()); | |||||
| break; | |||||
| case BOOLEAN: | |||||
| newCell.setCellValue(oldCell.getBooleanCellValue()); | |||||
| break; | |||||
| case ERROR: | |||||
| newCell.setCellErrorValue(oldCell.getErrorCellValue()); | |||||
| break; | |||||
| default: | |||||
| break; | |||||
| } | |||||
| for (int ii = 0; ii < sourceSheet.getNumMergedRegions(); ii++) { | |||||
| CellRangeAddress cellRangeAddress = sourceSheet.getMergedRegion(ii); | |||||
| if (cellRangeAddress.getFirstRow() == sourceRow.getRowNum()) { | |||||
| addMergedRegion( | |||||
| destinationSheet, | |||||
| cellRangeAddress.getFirstRow(), | |||||
| cellRangeAddress.getLastRow(), | |||||
| destinationColumnNum, | |||||
| (destinationColumnNum + (cellRangeAddress.getLastColumn() - cellRangeAddress.getFirstColumn()))); | |||||
| } | |||||
| } | |||||
| } | |||||
| destinationSheet.setColumnWidth(destinationColumnNum, sourceSheet.getColumnWidth(sourceColumnNum)); | |||||
| } | |||||
| /** | |||||
| * Copy Column | |||||
| * | |||||
| * @param workbook | |||||
| * The Excel workbook | |||||
| * @param sheet | |||||
| * The Excel worksheet | |||||
| * @param rowStart | |||||
| * The source row start index (0-based) to copy from | |||||
| * @param rowEnd | |||||
| * The source row end index (0-based) to copy from | |||||
| * @param sourceColumnNum | |||||
| * The source column index (0-based) to copy from | |||||
| * @param destinationColumnNum | |||||
| * The destination column index (0-based) to copy into (from the copied row) | |||||
| */ | |||||
| public static void copyColumn(Workbook workbook, Sheet sheet, int rowStart, int rowEnd, int sourceColumnNum, int destinationColumnNum) { | |||||
| copyColumn(workbook, sheet, sheet, rowStart, rowEnd, sourceColumnNum, destinationColumnNum); | |||||
| } | |||||
| public static void shiftColumns(Row row, int startingIndex, int shiftCount) { | |||||
| for (int i = row.getPhysicalNumberOfCells() - 1; i >= startingIndex; i--) { | |||||
| Cell oldCell = row.getCell(i); | |||||
| Cell newCell = row.createCell(i + shiftCount); | |||||
| // apply cell style to new cell from old cell | |||||
| newCell.setCellStyle(oldCell.getCellStyle()); | |||||
| // if there is a cell comment, copy | |||||
| if (oldCell.getCellComment() != null) { | |||||
| newCell.setCellComment(oldCell.getCellComment()); | |||||
| } | |||||
| // if there is a cell hyperlink, copy | |||||
| if (oldCell.getHyperlink() != null) { | |||||
| newCell.setHyperlink(oldCell.getHyperlink()); | |||||
| } | |||||
| // copy the cell data value | |||||
| switch (oldCell.getCellType()) { | |||||
| case NUMERIC: | |||||
| newCell.setCellValue(oldCell.getNumericCellValue()); | |||||
| break; | |||||
| case STRING: | |||||
| newCell.setCellValue(oldCell.getRichStringCellValue()); | |||||
| break; | |||||
| case FORMULA: | |||||
| newCell.setCellFormula(oldCell.getCellFormula()); | |||||
| break; | |||||
| case BLANK: | |||||
| newCell.setCellValue(oldCell.getStringCellValue()); | |||||
| break; | |||||
| case BOOLEAN: | |||||
| newCell.setCellValue(oldCell.getBooleanCellValue()); | |||||
| break; | |||||
| case ERROR: | |||||
| newCell.setCellErrorValue(oldCell.getErrorCellValue()); | |||||
| break; | |||||
| default: | |||||
| break; | |||||
| } | |||||
| } | |||||
| } | |||||
| /** handle some invalid char included ( /\*[]:? ) */ | |||||
| public static void setSheetName(Workbook workbook, Sheet sheet, String name) { | |||||
| if (workbook != null && sheet != null && StringUtils.isNotBlank(name)) | |||||
| workbook.setSheetName(workbook.getSheetIndex(sheet), name.replaceAll("[/\\\\*\\[\\]:\\?]", "_")); | |||||
| } | |||||
| /** delete row */ | |||||
| public static void deleteRow(Sheet sheet, int rowIndex) { | |||||
| if (sheet != null) { | |||||
| sheet.removeRow(sheet.getRow(rowIndex)); | |||||
| if (rowIndex < sheet.getLastRowNum()) | |||||
| sheet.shiftRows(rowIndex, sheet.getLastRowNum(), -1); | |||||
| } | |||||
| } | |||||
| public static byte[] encrypt(Workbook workbook, String password) { | |||||
| return encrypt(toByteArray(workbook), password); | |||||
| } | |||||
| public static byte[] encrypt(byte[] bytes, String password) { | |||||
| try { | |||||
| POIFSFileSystem fs = new POIFSFileSystem(); | |||||
| EncryptionInfo info = new EncryptionInfo(EncryptionMode.agile); | |||||
| // EncryptionInfo info = new EncryptionInfo(EncryptionMode.agile, CipherAlgorithm.aes192, HashAlgorithm.sha384, -1, -1, null); | |||||
| Encryptor enc = info.getEncryptor(); | |||||
| enc.confirmPassword(password); | |||||
| // Read in an existing OOXML file and write to encrypted output stream | |||||
| // don't forget to close the output stream otherwise the padding bytes aren't added | |||||
| OPCPackage opc = OPCPackage.open(new ByteArrayInputStream(bytes)); | |||||
| OutputStream os = enc.getDataStream(fs); | |||||
| opc.save(os); | |||||
| ByteArrayOutputStream bos = new ByteArrayOutputStream(); | |||||
| fs.writeFilesystem(bos); | |||||
| return bos.toByteArray(); | |||||
| } catch (Exception e) { | |||||
| throw new RuntimeException(e); | |||||
| } | |||||
| } | |||||
| public static Workbook loadTemplate(String templateClasspath) throws InvalidFormatException, IOException { | |||||
| return loadTemplateFile(templateClasspath); | |||||
| } | |||||
| public static Workbook loadTemplateFile(String templateClasspath) throws InvalidFormatException, IOException { | |||||
| ClassPathResource r = new ClassPathResource(templateClasspath + "_" + ".xlsx"); | |||||
| if (!r.exists()) r = new ClassPathResource(templateClasspath + ".xlsx"); | |||||
| try (InputStream in = r.getInputStream()) { | |||||
| return new XSSFWorkbook(in); | |||||
| } | |||||
| } | |||||
| public static void send(HttpServletResponse response, Workbook workbook, String filename) throws IOException { | |||||
| response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); | |||||
| response.setHeader("Content-Disposition", String.format("attachment; filename=\"%s\"", | |||||
| response.encodeURL(filename + ".xlsx"))); | |||||
| try (OutputStream out = response.getOutputStream()) { | |||||
| workbook.write(out); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,47 @@ | |||||
| package com.ffii.core.utils; | |||||
| import java.io.IOException; | |||||
| import com.fasterxml.jackson.core.JsonParseException; | |||||
| import com.fasterxml.jackson.core.JsonProcessingException; | |||||
| import com.fasterxml.jackson.databind.JsonMappingException; | |||||
| import com.fasterxml.jackson.databind.ObjectMapper; | |||||
| import com.fasterxml.jackson.databind.SerializationFeature; | |||||
| import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; | |||||
| /** | |||||
| * JSON Utils | |||||
| * | |||||
| * @author Patrick | |||||
| */ | |||||
| public abstract class JsonUtils { | |||||
| // Default mapper instance | |||||
| private static final ObjectMapper mapper = new ObjectMapper(); | |||||
| /** | |||||
| * Method that can be used to serialize any Java value as a JSON String. | |||||
| */ | |||||
| public static String toJsonString(Object obj) { | |||||
| try { | |||||
| mapper.registerModule(new JavaTimeModule()); | |||||
| mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); | |||||
| return mapper.writeValueAsString(obj); | |||||
| } catch (JsonProcessingException e) { | |||||
| return null; | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Read from JSON String. | |||||
| * | |||||
| * @param content | |||||
| * JSON String content | |||||
| * @param valueType | |||||
| * the return type | |||||
| */ | |||||
| public static <T> T fromJsonString(String content, Class<T> valueType) throws JsonParseException, JsonMappingException, IOException { | |||||
| return mapper.readValue(content, valueType); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,114 @@ | |||||
| package com.ffii.core.utils; | |||||
| import java.io.Serializable; | |||||
| import java.security.Key; | |||||
| import java.time.Instant; | |||||
| import java.util.Date; | |||||
| import java.util.HashMap; | |||||
| import java.util.Map; | |||||
| import java.util.function.Function; | |||||
| import org.slf4j.Logger; | |||||
| import org.slf4j.LoggerFactory; | |||||
| import org.springframework.beans.factory.config.ConfigurableBeanFactory; | |||||
| import org.springframework.context.annotation.Scope; | |||||
| import org.springframework.security.core.userdetails.UserDetails; | |||||
| import org.springframework.stereotype.Component; | |||||
| import com.ffii.tsms.model.RefreshToken; | |||||
| import io.jsonwebtoken.Claims; | |||||
| import io.jsonwebtoken.Jwts; | |||||
| import io.jsonwebtoken.SignatureAlgorithm; | |||||
| import io.jsonwebtoken.security.Keys; | |||||
| @Component | |||||
| @Scope(value = ConfigurableBeanFactory. SCOPE_SINGLETON) | |||||
| public class JwtTokenUtil implements Serializable { | |||||
| Logger logger = LoggerFactory.getLogger(JwtTokenUtil.class); | |||||
| private static final long serialVersionUID = -2550185165626007488L; | |||||
| // * 60000 = 1 Min | |||||
| public static final long JWT_TOKEN_EXPIRED_TIME = 60000 * 14400; | |||||
| public static final String AES_SECRET = "ffii"; | |||||
| public static final String TOKEN_SEPARATOR = "@@"; | |||||
| // @Value("${jwt.secret}") | |||||
| // private String secret; | |||||
| private static final Key secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS512); | |||||
| // retrieve username from jwt token | |||||
| public String getUsernameFromToken(String token) { | |||||
| return getClaimFromToken(token, Claims::getSubject); | |||||
| } | |||||
| // retrieve expiration date from jwt token | |||||
| public Date getExpirationDateFromToken(String token) { | |||||
| return getClaimFromToken(token, Claims::getExpiration); | |||||
| } | |||||
| public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) { | |||||
| final Claims claims = getAllClaimsFromToken(token); | |||||
| return claimsResolver.apply(claims); | |||||
| } | |||||
| // for retrieveing any information from token we will need the secret key | |||||
| private Claims getAllClaimsFromToken(String token) { | |||||
| return Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token).getBody(); | |||||
| } | |||||
| // check if the token has expired | |||||
| private Boolean isTokenExpired(String token) { | |||||
| final Date expiration = getExpirationDateFromToken(token); | |||||
| return expiration.before(new Date()); | |||||
| } | |||||
| // generate token for user | |||||
| public String generateToken(UserDetails userDetails) { | |||||
| Map<String, Object> claims = new HashMap<>(); | |||||
| return doGenerateToken(claims, userDetails.getUsername()); | |||||
| } | |||||
| // while creating the token - | |||||
| // 1. Define claims of the token, like Issuer, Expiration, Subject, and the ID | |||||
| // 2. Sign the JWT using the HS512 algorithm and secret key. | |||||
| // 3. According to JWS Compact | |||||
| // Serialization(https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41#section-3.1) | |||||
| // compaction of the JWT to a URL-safe string | |||||
| private String doGenerateToken(Map<String, Object> claims, String subject) { | |||||
| logger.info((new Date(System.currentTimeMillis() + JWT_TOKEN_EXPIRED_TIME)).toString()); | |||||
| return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis())) | |||||
| .setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_EXPIRED_TIME)) | |||||
| .signWith(secretKey).compact(); | |||||
| } | |||||
| // validate token | |||||
| public Boolean validateToken(String token, UserDetails userDetails) { | |||||
| final String username = getUsernameFromToken(token); | |||||
| return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); | |||||
| } | |||||
| public RefreshToken createRefreshToken(String username) { | |||||
| RefreshToken refreshToken = new RefreshToken(); | |||||
| refreshToken.setUserName(username); | |||||
| refreshToken.setExpiryDate(Instant.now().plusMillis(JWT_TOKEN_EXPIRED_TIME * 60 * 24)); | |||||
| long instantNum = Instant.now().plusMillis(JWT_TOKEN_EXPIRED_TIME * 60 * 24).toEpochMilli(); | |||||
| refreshToken.setToken(AES.encrypt(username + TOKEN_SEPARATOR + instantNum, AES_SECRET)); | |||||
| return refreshToken; | |||||
| } | |||||
| public boolean verifyExpiration(RefreshToken token) throws Exception { | |||||
| if (token.getExpiryDate().compareTo(Instant.now()) < 0) { | |||||
| return false; | |||||
| } | |||||
| return true; | |||||
| } | |||||
| public String getUsernameFromRefreshToken(String refreshToken) { | |||||
| return AES.decrypt(refreshToken, AES_SECRET); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,35 @@ | |||||
| package com.ffii.core.utils; | |||||
| import java.util.HashMap; | |||||
| import java.util.Map; | |||||
| /** | |||||
| * MapUtils | |||||
| * | |||||
| * @author Patrick | |||||
| */ | |||||
| public class MapUtils { | |||||
| /** | |||||
| * Convert key-value pairs to HashMap | |||||
| * | |||||
| * @param keyValuePairs | |||||
| * Keys and values must be in pairs | |||||
| * | |||||
| * @return Map | |||||
| */ | |||||
| @SuppressWarnings("unchecked") | |||||
| public static <K, V> Map<K, V> toHashMap(Object... keyValuePairs) { | |||||
| if (keyValuePairs.length % 2 != 0) | |||||
| throw new IllegalArgumentException("Keys and values must be in pairs"); | |||||
| Map<K, V> map = new HashMap<K, V>(keyValuePairs.length / 2); | |||||
| for (int i = 0; i < keyValuePairs.length; i += 2) { | |||||
| map.put((K) keyValuePairs[i], (V) keyValuePairs[i + 1]); | |||||
| } | |||||
| return map; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,42 @@ | |||||
| package com.ffii.core.utils; | |||||
| /** @author Alex */ | |||||
| public abstract class Params { | |||||
| public static final String ERROR = "error"; | |||||
| public static final String SUCCESS = "success"; | |||||
| public static final String DATA = "data"; | |||||
| public static final String RECORDS = "records"; | |||||
| public static final String TOTAL = "total"; | |||||
| public static final String ID = "id"; | |||||
| public static final String CODE = "code"; | |||||
| public static final String NAME = "name"; | |||||
| public static final String TYPE = "type"; | |||||
| public static final String MSG = "msg"; | |||||
| public static final String MSG_CODE = "msgCode"; | |||||
| public static final String MESSAGES = "messages"; | |||||
| public static final String FROM = "from"; | |||||
| public static final String TO = "to"; | |||||
| // sql | |||||
| public static final String QUERY = "query"; | |||||
| // pagin | |||||
| public static final String PAGE = "page"; | |||||
| public static final String START = "start"; | |||||
| public static final String LIMIT = "limit"; | |||||
| // filter | |||||
| public static final String FILTER = "filter"; | |||||
| public static final String OPERATOR = "operator"; | |||||
| public static final String LIKE = "like"; | |||||
| public static final String PROPERTY = "property"; | |||||
| // sort | |||||
| public static final String SORT = "sort"; | |||||
| public static final String DIRECTION = "direction"; | |||||
| public static final String VALUE = "value"; | |||||
| } | |||||
| @@ -0,0 +1,83 @@ | |||||
| /******************************************************************************* | |||||
| * Copyright 2Fi Business Solutions Ltd. | |||||
| * | |||||
| * This code is copyrighted. Under no circumstances should any party, people, | |||||
| * or organization should redistribute any portions of this code in any form, | |||||
| * either verbatim or through electronic media, to any third parties, unless | |||||
| * under explicit written permission by 2Fi Business Solutions Ltd. | |||||
| ******************************************************************************/ | |||||
| package com.ffii.core.utils; | |||||
| import java.util.ArrayList; | |||||
| import java.util.List; | |||||
| import java.util.Random; | |||||
| import java.util.regex.Pattern; | |||||
| public abstract class PasswordUtils { | |||||
| private static final Pattern PATTERN_DIGITS = Pattern.compile("[0-9]"); | |||||
| private static final Pattern PATTERN_A2Z_LOWER = Pattern.compile("[a-z]"); | |||||
| private static final Pattern PATTERN_A2Z_UPPER = Pattern.compile("[A-Z]"); | |||||
| private static final String A2Z_LOWER = "abcdefghijklmnopqrstuvwxyz"; | |||||
| private static final String A2Z_UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; | |||||
| private static final String DIGITS = "0123456789"; | |||||
| /* | |||||
| * Ref: https://www.owasp.org/index.php/Password_special_characters | |||||
| * without space character | |||||
| */ | |||||
| private static final String SPECIAL_CHARS = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"; | |||||
| private static Pattern PATTERN_SPECIAL_CHARS = Pattern.compile("[!\"#$%&'()*+,-./:;<=>?@\\[\\\\\\]^_`{|}~]"); | |||||
| public static final boolean checkPwd(String pwd, IPasswordRule rule) { | |||||
| if (pwd == null) return false; | |||||
| if (pwd.length() < rule.getMin()) return false; | |||||
| if (pwd.length() > rule.getMax()) return false; | |||||
| if (rule.needNumberChar() && !PATTERN_DIGITS.matcher(pwd).find()) return false; | |||||
| if (rule.needUpperEngChar() && !PATTERN_A2Z_UPPER.matcher(pwd).find()) return false; | |||||
| if (rule.needLowerEngChar() && !PATTERN_A2Z_LOWER.matcher(pwd).find()) return false; | |||||
| if (rule.needSpecialChar() && !PATTERN_SPECIAL_CHARS.matcher(pwd).find()) return false; | |||||
| return true; | |||||
| } | |||||
| public static String genPwd(IPasswordRule rule) { | |||||
| int length = rule.getMin(); | |||||
| StringBuilder password = new StringBuilder(length); | |||||
| Random random = new Random(System.nanoTime()); | |||||
| List<String> charCategories = new ArrayList<>(4); | |||||
| if (rule.needLowerEngChar()) charCategories.add(A2Z_LOWER); | |||||
| if (rule.needUpperEngChar()) charCategories.add(A2Z_UPPER); | |||||
| if (rule.needNumberChar()) charCategories.add(DIGITS); | |||||
| if (rule.needSpecialChar()) charCategories.add(SPECIAL_CHARS); | |||||
| for (int i = 0; i < length; i++) { | |||||
| String charCategory = charCategories.get(i % charCategories.size()); | |||||
| char randomChar = charCategory.charAt(random.nextInt(charCategory.length())); | |||||
| if (password.length() > 0) | |||||
| password.insert(random.nextInt(password.length()), randomChar); | |||||
| else | |||||
| password.append(randomChar); | |||||
| } | |||||
| return password.toString(); | |||||
| } | |||||
| public static interface IPasswordRule { | |||||
| public int getMin(); | |||||
| public int getMax(); | |||||
| public boolean needNumberChar(); | |||||
| public boolean needUpperEngChar(); | |||||
| public boolean needLowerEngChar(); | |||||
| public boolean needSpecialChar(); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,13 @@ | |||||
| package com.ffii.tsms; | |||||
| import org.springframework.boot.SpringApplication; | |||||
| import org.springframework.boot.autoconfigure.SpringBootApplication; | |||||
| @SpringBootApplication | |||||
| public class TsmsApplication { | |||||
| public static void main(String[] args) { | |||||
| SpringApplication.run(TsmsApplication.class, args); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,36 @@ | |||||
| package com.ffii.tsms.config; | |||||
| import javax.sql.DataSource; | |||||
| import org.springframework.boot.context.properties.ConfigurationProperties; | |||||
| 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.scheduling.annotation.EnableAsync; | |||||
| import org.springframework.scheduling.annotation.EnableScheduling; | |||||
| import com.ffii.core.support.JdbcDao; | |||||
| /** @author Terence */ | |||||
| @Configuration | |||||
| // @EnableJpaRepositories("com.ffii.ars.*") | |||||
| // @ComponentScan(basePackages = { "com.ffii.core.*" }) | |||||
| @ComponentScan(basePackages = { "com.ffii.core.*","com.ffii.tsms.*"}) | |||||
| // @EntityScan("com.ffii.ars.*") | |||||
| @EnableScheduling | |||||
| @EnableAsync | |||||
| public class AppConfig { | |||||
| @Bean | |||||
| @ConfigurationProperties(prefix = "spring.datasource") | |||||
| public DataSource dataSource() { | |||||
| return DataSourceBuilder.create().build(); | |||||
| } | |||||
| @Bean | |||||
| public JdbcDao jdbcDao(DataSource dataSource) { | |||||
| return new JdbcDao(dataSource); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,29 @@ | |||||
| package com.ffii.tsms.config; | |||||
| import org.springframework.context.annotation.Bean; | |||||
| import org.springframework.context.annotation.Configuration; | |||||
| import org.springframework.web.servlet.config.annotation.CorsRegistry; | |||||
| import org.springframework.web.servlet.config.annotation.EnableWebMvc; | |||||
| import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; | |||||
| import org.springframework.web.servlet.view.InternalResourceViewResolver; | |||||
| @Configuration | |||||
| @EnableWebMvc | |||||
| public class WebConfig implements WebMvcConfigurer { | |||||
| @Override | |||||
| public void addCorsMappings(CorsRegistry registry) { | |||||
| registry.addMapping("/**") | |||||
| .allowedHeaders("*") | |||||
| .allowedOrigins("*") | |||||
| .exposedHeaders("filename") | |||||
| .allowedMethods("GET", "POST", "PUT", "DELETE", "HEAD"); | |||||
| } | |||||
| @Bean | |||||
| public InternalResourceViewResolver defaultViewResolver() { | |||||
| return new InternalResourceViewResolver(); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,80 @@ | |||||
| package com.ffii.tsms.config.security; | |||||
| import org.springframework.beans.factory.annotation.Autowired; | |||||
| import org.springframework.beans.factory.annotation.Qualifier; | |||||
| import org.springframework.context.annotation.Bean; | |||||
| import org.springframework.context.annotation.Configuration; | |||||
| import org.springframework.context.annotation.Lazy; | |||||
| import org.springframework.core.annotation.Order; | |||||
| import org.springframework.http.HttpStatus; | |||||
| import org.springframework.ldap.core.support.BaseLdapPathContextSource; | |||||
| import org.springframework.security.authentication.AuthenticationManager; | |||||
| import org.springframework.security.config.Customizer; | |||||
| import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; | |||||
| import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; | |||||
| import org.springframework.security.config.annotation.web.builders.HttpSecurity; | |||||
| import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; | |||||
| import org.springframework.security.config.http.SessionCreationPolicy; | |||||
| import org.springframework.security.config.ldap.LdapBindAuthenticationManagerFactory; | |||||
| import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; | |||||
| import org.springframework.security.crypto.password.PasswordEncoder; | |||||
| import org.springframework.security.web.SecurityFilterChain; | |||||
| import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; | |||||
| import com.ffii.tsms.config.security.jwt.JwtRequestFilter; | |||||
| @Configuration | |||||
| @EnableWebSecurity | |||||
| @EnableMethodSecurity | |||||
| public class SecurityConfig { | |||||
| public static final String INDEX_URL = "/"; | |||||
| public static final String LOGIN_URL = "/login"; | |||||
| public static final String LDAP_LOGIN_URL = "/ldap-login"; | |||||
| public static final String[] URL_WHITELIST = { | |||||
| INDEX_URL, | |||||
| LOGIN_URL, | |||||
| LDAP_LOGIN_URL | |||||
| }; | |||||
| @Lazy | |||||
| @Autowired | |||||
| private JwtRequestFilter jwtRequestFilter; | |||||
| @Bean | |||||
| @Qualifier("AuthenticationManager") | |||||
| public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) | |||||
| throws Exception { | |||||
| return authenticationConfiguration.getAuthenticationManager(); | |||||
| } | |||||
| @Bean | |||||
| @Qualifier("LdapAuthenticationManager") | |||||
| public AuthenticationManager ldapAuthenticationManager(BaseLdapPathContextSource contextSource) { | |||||
| LdapBindAuthenticationManagerFactory factory = new LdapBindAuthenticationManagerFactory(contextSource); | |||||
| factory.setUserSearchFilter("cn={0}"); | |||||
| return factory.createAuthenticationManager(); | |||||
| } | |||||
| @Bean | |||||
| public PasswordEncoder passwordEncoder() { | |||||
| return new BCryptPasswordEncoder(); | |||||
| } | |||||
| @Bean | |||||
| @Order(1) | |||||
| public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { | |||||
| return http | |||||
| .cors(Customizer.withDefaults()).csrf(csrf -> csrf.disable()) | |||||
| .requestCache(requestCache -> requestCache.disable()) | |||||
| .authorizeHttpRequests( | |||||
| authRequest -> authRequest.requestMatchers(URL_WHITELIST).permitAll().anyRequest().authenticated()) | |||||
| .httpBasic(httpBasic -> httpBasic.authenticationEntryPoint( | |||||
| (request, response, authException) -> response.sendError(HttpStatus.UNAUTHORIZED.value()))) | |||||
| .sessionManagement( | |||||
| sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) | |||||
| .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class) | |||||
| .build(); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,75 @@ | |||||
| package com.ffii.tsms.config.security.jwt; | |||||
| import java.io.IOException; | |||||
| import org.springframework.beans.factory.annotation.Autowired; | |||||
| import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | |||||
| import org.springframework.security.core.context.SecurityContextHolder; | |||||
| import org.springframework.security.core.userdetails.UserDetails; | |||||
| import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; | |||||
| import org.springframework.stereotype.Component; | |||||
| import org.springframework.web.filter.OncePerRequestFilter; | |||||
| import com.ffii.core.utils.JwtTokenUtil; | |||||
| import com.ffii.tsms.config.security.jwt.service.JwtUserDetailsService; | |||||
| import io.jsonwebtoken.ExpiredJwtException; | |||||
| import jakarta.servlet.FilterChain; | |||||
| import jakarta.servlet.ServletException; | |||||
| import jakarta.servlet.http.HttpServletRequest; | |||||
| import jakarta.servlet.http.HttpServletResponse; | |||||
| @Component | |||||
| public class JwtRequestFilter extends OncePerRequestFilter { | |||||
| @Autowired | |||||
| private JwtUserDetailsService jwtUserDetailsService; | |||||
| @Autowired | |||||
| private JwtTokenUtil jwtTokenUtil; | |||||
| @Override | |||||
| protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) | |||||
| throws ServletException, IOException { | |||||
| final String requestTokenHeader = request.getHeader("Authorization"); | |||||
| String username = null; | |||||
| String jwtToken = null; | |||||
| // JWT Token is in the form "Bearer token". Remove Bearer word and get | |||||
| // only the Token | |||||
| if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) { | |||||
| jwtToken = requestTokenHeader.substring(7).replaceAll("\"", ""); | |||||
| try { | |||||
| username = jwtTokenUtil.getUsernameFromToken(jwtToken); | |||||
| } catch (IllegalArgumentException e) { | |||||
| logger.error("Unable to get JWT Token"); | |||||
| } catch (ExpiredJwtException e) { | |||||
| logger.error("JWT Token has expired"); | |||||
| } | |||||
| } else { | |||||
| logger.warn("JWT Token does not begin with Bearer String"); | |||||
| } | |||||
| // Once we get the token validate it. | |||||
| if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { | |||||
| UserDetails userDetails = jwtUserDetailsService.loadUserByUsername(username); | |||||
| // if token is valid configure Spring Security to manually set | |||||
| // authentication | |||||
| if (jwtTokenUtil.validateToken(jwtToken, userDetails)) { | |||||
| UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken( | |||||
| userDetails, null, userDetails.getAuthorities()); | |||||
| usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); | |||||
| // After setting the Authentication in the context, we specify | |||||
| // that the current user is authenticated. So it passes the | |||||
| // Spring Security Configurations successfully. | |||||
| SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken); | |||||
| } | |||||
| } | |||||
| chain.doFilter(request, response); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,31 @@ | |||||
| package com.ffii.tsms.config.security.jwt.service; | |||||
| import org.springframework.beans.factory.annotation.Autowired; | |||||
| import org.springframework.security.core.userdetails.UserDetailsService; | |||||
| import org.springframework.security.core.userdetails.UsernameNotFoundException; | |||||
| import org.springframework.stereotype.Service; | |||||
| import com.ffii.tsms.modules.user.entity.User; | |||||
| import com.ffii.tsms.modules.user.entity.UserRepository; | |||||
| import com.ffii.tsms.modules.user.service.UserAuthorityService; | |||||
| import com.ffii.tsms.modules.user.service.UserService; | |||||
| @Service | |||||
| public class JwtUserDetailsService implements UserDetailsService { | |||||
| @Autowired | |||||
| UserRepository userRepository; | |||||
| @Autowired | |||||
| UserAuthorityService userAuthService; | |||||
| @Autowired | |||||
| UserService userService; | |||||
| @Override | |||||
| public User loadUserByUsername(String username) throws UsernameNotFoundException { | |||||
| return userService.loadUserOptByUsername(username).orElseThrow(() -> new UsernameNotFoundException(username)); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,151 @@ | |||||
| package com.ffii.tsms.config.security.jwt.web; | |||||
| import java.time.Instant; | |||||
| import java.util.HashSet; | |||||
| import java.util.Set; | |||||
| import org.apache.commons.lang3.exception.ExceptionUtils; | |||||
| import org.springframework.beans.factory.annotation.Autowired; | |||||
| import org.springframework.beans.factory.annotation.Qualifier; | |||||
| import org.springframework.http.HttpStatus; | |||||
| import org.springframework.http.ResponseEntity; | |||||
| import org.springframework.security.authentication.AuthenticationManager; | |||||
| import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | |||||
| import org.springframework.security.core.userdetails.UserDetails; | |||||
| import org.springframework.web.bind.annotation.CrossOrigin; | |||||
| import org.springframework.web.bind.annotation.PostMapping; | |||||
| import org.springframework.web.bind.annotation.RequestBody; | |||||
| import org.springframework.web.bind.annotation.RestController; | |||||
| import org.springframework.web.server.ResponseStatusException; | |||||
| import com.ffii.core.utils.AES; | |||||
| import com.ffii.core.utils.JwtTokenUtil; | |||||
| import com.ffii.tsms.config.security.jwt.service.JwtUserDetailsService; | |||||
| import com.ffii.tsms.config.security.service.LoginLogService; | |||||
| import com.ffii.tsms.model.AbilityModel; | |||||
| import com.ffii.tsms.model.ExceptionResponse; | |||||
| import com.ffii.tsms.model.JwtRequest; | |||||
| import com.ffii.tsms.model.JwtResponse; | |||||
| import com.ffii.tsms.model.RefreshToken; | |||||
| import com.ffii.tsms.model.TokenRefreshRequest; | |||||
| import com.ffii.tsms.model.TokenRefreshResponse; | |||||
| import com.ffii.tsms.modules.common.SecurityUtils; | |||||
| import com.ffii.tsms.modules.user.entity.User; | |||||
| import com.ffii.tsms.modules.user.entity.UserRepository; | |||||
| import com.ffii.tsms.modules.user.service.UserAuthorityService; | |||||
| import jakarta.servlet.http.HttpServletRequest; | |||||
| import jakarta.validation.Valid; | |||||
| @RestController | |||||
| @CrossOrigin(origins = "*", allowedHeaders = "*") | |||||
| public class JwtAuthenticationController { | |||||
| @Autowired | |||||
| @Qualifier("AuthenticationManager") | |||||
| private AuthenticationManager authenticationManager; | |||||
| @Autowired | |||||
| @Qualifier("LdapAuthenticationManager") | |||||
| private AuthenticationManager ldapAuthenticationManager; | |||||
| @Autowired | |||||
| private JwtTokenUtil jwtTokenUtil; | |||||
| @Autowired | |||||
| private JwtUserDetailsService userDetailsService; | |||||
| @Autowired | |||||
| private UserRepository userRepository; | |||||
| @Autowired | |||||
| UserAuthorityService userAuthorityService; | |||||
| @Autowired | |||||
| LoginLogService loginLogService; | |||||
| @PostMapping("/login") | |||||
| public ResponseEntity<?> login(@RequestBody JwtRequest authenticationRequest, HttpServletRequest request) throws Exception { | |||||
| String username = authenticationRequest.getUsername(); | |||||
| try { | |||||
| boolean success = authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword()); | |||||
| loginLogService.createLoginLog(username, request.getRemoteAddr(), success); | |||||
| } catch (Exception e) { | |||||
| if (username != null) { | |||||
| loginLogService.createLoginLog(username, request.getRemoteAddr(), false); | |||||
| } | |||||
| return ResponseEntity.status(HttpStatus.UNAUTHORIZED) | |||||
| .body(new ExceptionResponse("Unauthorized", ExceptionUtils.getStackTrace(e))); | |||||
| } | |||||
| return createAuthTokenResponse(authenticationRequest); | |||||
| } | |||||
| @PostMapping("/ldap-login") | |||||
| public ResponseEntity<?> ldapLogin(@RequestBody JwtRequest authenticationRequest, HttpServletRequest request) throws Exception { | |||||
| String username = authenticationRequest.getUsername(); | |||||
| try { | |||||
| boolean success = ldapAuthenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword()); | |||||
| loginLogService.createLoginLog(username, request.getRemoteAddr(), success); | |||||
| } catch (Exception e) { | |||||
| loginLogService.createLoginLog(username, request.getRemoteAddr(), false); | |||||
| return ResponseEntity.status(HttpStatus.UNAUTHORIZED) | |||||
| .body(new ExceptionResponse("Unauthorized", ExceptionUtils.getStackTrace(e))); | |||||
| } | |||||
| return createAuthTokenResponse(authenticationRequest); | |||||
| } | |||||
| private boolean authenticate(String username, String password) throws Exception { | |||||
| authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password)); | |||||
| return true; | |||||
| } | |||||
| private boolean ldapAuthenticate(String username, String password) throws Exception { | |||||
| ldapAuthenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password)); | |||||
| return true; | |||||
| } | |||||
| private ResponseEntity<?> createAuthTokenResponse(JwtRequest authenticationRequest) { | |||||
| final UserDetails userDetails = userDetailsService.loadUserByUsername(authenticationRequest.getUsername()); | |||||
| if (userDetails == null) { | |||||
| return ResponseEntity.status(HttpStatus.UNAUTHORIZED) | |||||
| .body(new ExceptionResponse(authenticationRequest.getUsername() + " not yet register in the system.", null)); | |||||
| } | |||||
| final String accessToken = jwtTokenUtil.generateToken(userDetails); | |||||
| final String refreshToken = jwtTokenUtil.createRefreshToken(userDetails.getUsername()).getToken(); | |||||
| User user = userRepository.findByName(authenticationRequest.getUsername()).get(0); | |||||
| Set<AbilityModel> abilities = new HashSet<>(); | |||||
| userAuthorityService.getUserAuthority(user).forEach(auth -> abilities.add(new AbilityModel(auth.getAuthority()))); | |||||
| return ResponseEntity.ok(new JwtResponse(accessToken, refreshToken, null, user, abilities)); | |||||
| } | |||||
| @PostMapping("/refresh-token") | |||||
| public ResponseEntity<TokenRefreshResponse> refreshtoken(@Valid @RequestBody TokenRefreshRequest request) | |||||
| throws Exception { | |||||
| String requestRefreshToken = request.getRefreshToken(); | |||||
| requestRefreshToken = requestRefreshToken.replaceAll("\"", ""); | |||||
| String[] decryptStringList = AES.decrypt(requestRefreshToken, JwtTokenUtil.AES_SECRET) | |||||
| .split(JwtTokenUtil.TOKEN_SEPARATOR); | |||||
| RefreshToken instance = new RefreshToken(); | |||||
| String username = decryptStringList[0]; | |||||
| instance.setExpiryDate(Instant.ofEpochMilli(Long.valueOf(decryptStringList[1]))); | |||||
| instance.setToken(requestRefreshToken); | |||||
| instance.setUserName(decryptStringList[0]); | |||||
| if (!jwtTokenUtil.verifyExpiration(instance)) { | |||||
| throw new ResponseStatusException(HttpStatus.EXPECTATION_FAILED, | |||||
| "Refresh token was expired. Please make a new signin request"); | |||||
| } | |||||
| final UserDetails userDetails = userDetailsService.loadUserByUsername(username); | |||||
| String accessToken = jwtTokenUtil.generateToken(userDetails); | |||||
| String refreshToken = jwtTokenUtil.createRefreshToken(username).getToken(); | |||||
| return ResponseEntity.ok(new TokenRefreshResponse(accessToken, refreshToken)); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,43 @@ | |||||
| package com.ffii.tsms.config.security.service; | |||||
| import java.util.Date; | |||||
| import java.util.HashMap; | |||||
| import java.util.List; | |||||
| import java.util.Map; | |||||
| import org.springframework.beans.factory.annotation.Autowired; | |||||
| import org.springframework.stereotype.Service; | |||||
| import org.springframework.transaction.annotation.Isolation; | |||||
| import org.springframework.transaction.annotation.Transactional; | |||||
| import com.ffii.core.support.AbstractService; | |||||
| import com.ffii.core.support.JdbcDao; | |||||
| import com.ffii.core.utils.MapUtils; | |||||
| @Service | |||||
| public class LoginLogService extends AbstractService { | |||||
| public LoginLogService(JdbcDao jdbcDao) { | |||||
| super(jdbcDao); | |||||
| } | |||||
| @Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class, readOnly = false) | |||||
| public boolean createLoginLog(String username, String remoteAddr, boolean success) { | |||||
| String sql = "INSERT INTO user_login_log (`username`, `loginTime`, `ipAddr`, `success`) " | |||||
| +"VALUES (:username, :loginTime, :ipAddr, :success)"; | |||||
| Map<String, Object> args = new HashMap<>(4); | |||||
| args.put("username", username); | |||||
| args.put("loginTime", new Date()); | |||||
| args.put("ipAddr", remoteAddr); | |||||
| args.put("success", success); | |||||
| return (jdbcDao.executeUpdate(sql, args) == 1); | |||||
| } | |||||
| @Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class, readOnly = true) | |||||
| public List<Map<String, Object>> listLastLog(String username, int limit) { | |||||
| return jdbcDao.queryForList("SELECT success FROM user_login_log where username = :username ORDER BY loginTime DESC LIMIT " + limit, | |||||
| MapUtils.toHashMap("username", username)); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,13 @@ | |||||
| package com.ffii.tsms.model; | |||||
| public class AbilityModel { | |||||
| private final String actionSubjectCombo; | |||||
| public AbilityModel(String actionSubjectCombo) { | |||||
| this.actionSubjectCombo = actionSubjectCombo; | |||||
| } | |||||
| public String getActionSubjectCombo() { | |||||
| return actionSubjectCombo; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,28 @@ | |||||
| package com.ffii.tsms.model; | |||||
| public class ExceptionResponse { | |||||
| private String message; | |||||
| private String exception; | |||||
| public ExceptionResponse(String message, String exception) { | |||||
| this.message = message; | |||||
| this.exception = exception; | |||||
| } | |||||
| public String getMessage() { | |||||
| return message; | |||||
| } | |||||
| public void setMessage(String message) { | |||||
| this.message = message; | |||||
| } | |||||
| public String getException() { | |||||
| return exception; | |||||
| } | |||||
| public void setException(String exception) { | |||||
| this.exception = exception; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,40 @@ | |||||
| package com.ffii.tsms.model; | |||||
| import java.io.Serializable; | |||||
| public class JwtRequest implements Serializable { | |||||
| private static final long serialVersionUID = 5926468583005150707L; | |||||
| private String username; | |||||
| private String password; | |||||
| //need default constructor for JSON Parsing | |||||
| public JwtRequest() | |||||
| { | |||||
| } | |||||
| public JwtRequest(String username, String password) { | |||||
| this.setUsername(username); | |||||
| this.setPassword(password); | |||||
| } | |||||
| public String getUsername() { | |||||
| return this.username; | |||||
| } | |||||
| public void setUsername(String username) { | |||||
| this.username = username; | |||||
| } | |||||
| public String getPassword() { | |||||
| return this.password; | |||||
| } | |||||
| public void setPassword(String password) { | |||||
| this.password = password; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,56 @@ | |||||
| package com.ffii.tsms.model; | |||||
| import java.io.Serializable; | |||||
| import java.util.Set; | |||||
| import com.ffii.tsms.modules.user.entity.User; | |||||
| public class JwtResponse implements Serializable { | |||||
| private static final long serialVersionUID = -8091879091924046844L; | |||||
| private final Long id; | |||||
| private final String name; | |||||
| private final String email; | |||||
| private final String accessToken; | |||||
| private final String refreshToken; | |||||
| private final String role; | |||||
| private final Set<AbilityModel> abilities; | |||||
| public JwtResponse(String accessToken, String refreshToken, String role, User user, Set<AbilityModel> abilities) { | |||||
| this.accessToken = accessToken; | |||||
| this.refreshToken = refreshToken; | |||||
| this.role = role; | |||||
| this.id = user.getId(); | |||||
| this.name = user.getName(); | |||||
| this.email = user.getEmail(); | |||||
| this.abilities = abilities; | |||||
| } | |||||
| public String getAccessToken() { | |||||
| return this.accessToken; | |||||
| } | |||||
| public String getRole() { | |||||
| return role; | |||||
| } | |||||
| public String getRefreshToken() { | |||||
| return refreshToken; | |||||
| } | |||||
| public Long getId() { | |||||
| return id; | |||||
| } | |||||
| public String getName() { | |||||
| return name; | |||||
| } | |||||
| public String getEmail() { | |||||
| return email; | |||||
| } | |||||
| public Set<AbilityModel> getAbilities() { | |||||
| return abilities; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,39 @@ | |||||
| package com.ffii.tsms.model; | |||||
| import java.time.Instant; | |||||
| public class RefreshToken { | |||||
| private String userName; | |||||
| private String token; | |||||
| private Instant expiryDate; | |||||
| public String getUserName() { | |||||
| return userName; | |||||
| } | |||||
| public void setUserName(String userName) { | |||||
| this.userName = userName; | |||||
| } | |||||
| public String getToken() { | |||||
| return token; | |||||
| } | |||||
| public void setToken(String token) { | |||||
| this.token = token; | |||||
| } | |||||
| public Instant getExpiryDate() { | |||||
| return expiryDate; | |||||
| } | |||||
| public void setExpiryDate(Instant expiryDate) { | |||||
| this.expiryDate = expiryDate; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,16 @@ | |||||
| package com.ffii.tsms.model; | |||||
| import jakarta.validation.constraints.NotBlank; | |||||
| public class TokenRefreshRequest { | |||||
| @NotBlank | |||||
| private String refreshToken; | |||||
| public String getRefreshToken() { | |||||
| return refreshToken; | |||||
| } | |||||
| public void setRefreshToken(String refreshToken) { | |||||
| this.refreshToken = refreshToken; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,37 @@ | |||||
| package com.ffii.tsms.model; | |||||
| public class TokenRefreshResponse { | |||||
| private String accessToken; | |||||
| private String refreshToken; | |||||
| private String tokenType = "Bearer"; | |||||
| public TokenRefreshResponse(String accessToken, String refreshToken) { | |||||
| this.accessToken = accessToken; | |||||
| this.refreshToken = refreshToken; | |||||
| } | |||||
| public String getAccessToken() { | |||||
| return accessToken; | |||||
| } | |||||
| public void setAccessToken(String accessToken) { | |||||
| this.accessToken = accessToken; | |||||
| } | |||||
| public String getRefreshToken() { | |||||
| return refreshToken; | |||||
| } | |||||
| public void setRefreshToken(String refreshToken) { | |||||
| this.refreshToken = refreshToken; | |||||
| } | |||||
| public String getTokenType() { | |||||
| return tokenType; | |||||
| } | |||||
| public void setTokenType(String tokenType) { | |||||
| this.tokenType = tokenType; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,17 @@ | |||||
| package com.ffii.tsms.modules.common; | |||||
| public class ErrorCodes { | |||||
| public static final String FILE_UPLOAD_ERROR = "FILE_UPLOAD_ERROR"; | |||||
| public static final String STOCK_IN_WRONG_POST = "STOCK_IN_WRONG_POST"; | |||||
| public static final String USER_WRONG_NEW_PWD = "USER_WRONG_NEW_PWD"; | |||||
| public static final String SEND_EMAIL_ERROR = "SEND_EMAIL_ERROR"; | |||||
| public static final String USERNAME_NOT_AVAILABLE = "USERNAME_NOT_AVAILABLE"; | |||||
| public static final String INIT_EXCEL_ERROR = "INIT_EXCEL_ERROR"; | |||||
| public static final String CHANGE_MAIN_CUSTOMER_ERROR = "CHANGE_MAIN_CUSTOMER_ERROR"; | |||||
| } | |||||
| @@ -0,0 +1,116 @@ | |||||
| package com.ffii.tsms.modules.common; | |||||
| import com.fasterxml.jackson.annotation.JsonIgnore; | |||||
| import com.ffii.core.utils.PasswordUtils.IPasswordRule; | |||||
| import com.ffii.tsms.modules.settings.service.SettingsService; | |||||
| public class PasswordRule implements IPasswordRule { | |||||
| private Integer min; | |||||
| private Integer max; | |||||
| private Boolean number; | |||||
| private Boolean upperEng; | |||||
| private Boolean lowerEng; | |||||
| private Boolean specialChar; | |||||
| public PasswordRule(SettingsService settingsService) { | |||||
| if (settingsService == null){ | |||||
| throw new IllegalArgumentException("settingsService"); | |||||
| } | |||||
| this.min = settingsService.getInt(SettingNames.SYS_PASSWORD_RULE_MIN); | |||||
| this.max = settingsService.getInt(SettingNames.SYS_PASSWORD_RULE_MAX); | |||||
| this.number = settingsService.getBoolean(SettingNames.SYS_PASSWORD_RULE_NUMBER); | |||||
| this.upperEng = settingsService.getBoolean(SettingNames.SYS_PASSWORD_RULE_UPPER_ENG); | |||||
| this.lowerEng = settingsService.getBoolean(SettingNames.SYS_PASSWORD_RULE_LOWER_ENG); | |||||
| this.specialChar = settingsService.getBoolean(SettingNames.SYS_PASSWORD_RULE_SPECIAL); | |||||
| } | |||||
| @Override | |||||
| public int getMin() { | |||||
| return min; | |||||
| } | |||||
| @Override | |||||
| public int getMax() { | |||||
| return max; | |||||
| } | |||||
| @Override | |||||
| public boolean needNumberChar() { | |||||
| return number; | |||||
| } | |||||
| @Override | |||||
| public boolean needUpperEngChar() { | |||||
| return upperEng; | |||||
| } | |||||
| @Override | |||||
| public boolean needLowerEngChar() { | |||||
| return lowerEng; | |||||
| } | |||||
| @Override | |||||
| public boolean needSpecialChar() { | |||||
| return specialChar; | |||||
| } | |||||
| public void setMin(Integer min) { | |||||
| this.min = min; | |||||
| } | |||||
| public void setMax(Integer max) { | |||||
| this.max = max; | |||||
| } | |||||
| public Boolean getNumber() { | |||||
| return number; | |||||
| } | |||||
| public void setNumber(Boolean number) { | |||||
| this.number = number; | |||||
| } | |||||
| public Boolean getUpperEng() { | |||||
| return upperEng; | |||||
| } | |||||
| public void setUpperEng(Boolean upperEng) { | |||||
| this.upperEng = upperEng; | |||||
| } | |||||
| public Boolean getLowerEng() { | |||||
| return lowerEng; | |||||
| } | |||||
| public void setLowerEng(Boolean lowerEng) { | |||||
| this.lowerEng = lowerEng; | |||||
| } | |||||
| public Boolean getSpecialChar() { | |||||
| return specialChar; | |||||
| } | |||||
| public void setSpecialChar(Boolean specialChar) { | |||||
| this.specialChar = specialChar; | |||||
| } | |||||
| @JsonIgnore | |||||
| public String getWrongMsg() { | |||||
| StringBuilder msg = new StringBuilder("Please Following Password Rule.\n"); | |||||
| msg.append("Minimum " + getMin() + " Characters\n"); | |||||
| msg.append("Maximum " + getMax() + " Characters\n"); | |||||
| if (needNumberChar()) | |||||
| msg.append("Numbers\n"); | |||||
| if (needLowerEngChar()) | |||||
| msg.append("Lower-Case Letters\n"); | |||||
| if (needUpperEngChar()) | |||||
| msg.append("Capital Letters\n"); | |||||
| if (needSpecialChar()) | |||||
| msg.append("Symbols\n"); | |||||
| return msg.toString(); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,146 @@ | |||||
| package com.ffii.tsms.modules.common; | |||||
| import java.util.Optional; | |||||
| import org.springframework.dao.DataAccessException; | |||||
| import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | |||||
| import org.springframework.security.core.Authentication; | |||||
| import org.springframework.security.core.GrantedAuthority; | |||||
| import org.springframework.security.core.context.SecurityContext; | |||||
| import org.springframework.security.core.context.SecurityContextHolder; | |||||
| import org.springframework.security.core.userdetails.UserDetails; | |||||
| import org.springframework.security.core.userdetails.UserDetailsService; | |||||
| import org.springframework.security.core.userdetails.UsernameNotFoundException; | |||||
| import com.ffii.tsms.modules.user.entity.User; | |||||
| /** | |||||
| * Security Utils - for Spring Security | |||||
| * | |||||
| * @author Patrick | |||||
| */ | |||||
| public class SecurityUtils { | |||||
| /** | |||||
| * Obtains the current {@code SecurityContext}. | |||||
| * | |||||
| * @return the security context (never {@code null}) | |||||
| */ | |||||
| public static final SecurityContext getSecurityContext() { | |||||
| return SecurityContextHolder.getContext(); | |||||
| } | |||||
| /** | |||||
| * @return the authenticated {@code Principal}) | |||||
| * @see Authentication#getPrincipal() | |||||
| */ | |||||
| public static final Optional<User> getUser() { | |||||
| try { | |||||
| return Optional.of((User) getSecurityContext().getAuthentication().getPrincipal()); | |||||
| } catch (ClassCastException e) { | |||||
| // no authenticated principal | |||||
| return Optional.empty(); | |||||
| } catch (NullPointerException e) { | |||||
| // no authentication information is available | |||||
| return Optional.empty(); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Updates the Authentication Token with the user (e.g. user changed the password) | |||||
| * | |||||
| * @see SecurityContext#setAuthentication(Authentication) | |||||
| */ | |||||
| public static final void updateUserAuthentication(final UserDetails user) { | |||||
| getSecurityContext().setAuthentication(new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities())); | |||||
| } | |||||
| /** | |||||
| * Checks if the current user is GRANTED the {@code role} | |||||
| * | |||||
| * @param role | |||||
| * the {@code role} to check for | |||||
| * @return {@code true} if the current user is GRANTED the {@code role}, else {@code false} | |||||
| */ | |||||
| public static final boolean isGranted(String role) { | |||||
| Authentication authentication = getSecurityContext().getAuthentication(); | |||||
| if (authentication == null) return false; | |||||
| for (GrantedAuthority auth : authentication.getAuthorities()) { | |||||
| if (role.equals(auth.getAuthority())) return true; | |||||
| } | |||||
| return false; | |||||
| } | |||||
| /** | |||||
| * Checks if the current user is NOT GRANTED the {@code role} | |||||
| * | |||||
| * @param role | |||||
| * the {@code role} to check for | |||||
| * @return {@code true} if the current user is NOT GRANTED the {@code role}, else {@code false} | |||||
| */ | |||||
| public static final boolean isNotGranted(String role) { | |||||
| return !isGranted(role); | |||||
| } | |||||
| /** | |||||
| * Checks if the current user is GRANTED ANY of the {@code role}s | |||||
| * | |||||
| * @param roles | |||||
| * the {@code role}s to check for | |||||
| * @return {@code true} if the current user is GRANTED ANY of the {@code role}s, else {@code false} | |||||
| */ | |||||
| public static final boolean isGrantedAny(String... roles) { | |||||
| for (int i = 0; i < roles.length; i++) { | |||||
| if (isGranted(roles[i])) return true; | |||||
| } | |||||
| return false; | |||||
| } | |||||
| /** | |||||
| * Checks if the current user is NOT GRANTED ANY of the {@code role}s | |||||
| * | |||||
| * @param roles | |||||
| * the {@code role}s to check for | |||||
| * @return {@code true} if the current user is NOT GRANTED ANY of the {@code role}s, else {@code false} | |||||
| */ | |||||
| public static final boolean isNotGrantedAny(String... roles) { | |||||
| return !isGrantedAny(roles); | |||||
| } | |||||
| /** | |||||
| * Checks if the current user is GRANTED ALL of the {@code role}s | |||||
| * | |||||
| * @param roles | |||||
| * the {@code role}s to check for | |||||
| * @return {@code true} if the current user is GRANTED ALL of the {@code role}s, else {@code false} | |||||
| */ | |||||
| public static final boolean isGrantedAll(String... roles) { | |||||
| for (int i = 0; i < roles.length; i++) { | |||||
| if (isNotGranted(roles[i])) return false; | |||||
| } | |||||
| return true; | |||||
| } | |||||
| /** | |||||
| * Login a user non-interactively | |||||
| * | |||||
| * @param userService | |||||
| * any implementation of {@link UserDetailsService} | |||||
| * @param username | |||||
| * the username | |||||
| * | |||||
| * @throws UsernameNotFoundException | |||||
| * if the user could not be found or the user has no GrantedAuthority | |||||
| * @throws DataAccessException | |||||
| * if user could not be found for a repository-specific reason | |||||
| */ | |||||
| public static final void loginUser(UserDetailsService userService, String username) { | |||||
| /* load the user, throw exception if user not found */ | |||||
| UserDetails userDetails = userService.loadUserByUsername(username); | |||||
| /* create authentication token for the specified user */ | |||||
| Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities()); | |||||
| getSecurityContext().setAuthentication(authentication); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,61 @@ | |||||
| package com.ffii.tsms.modules.common; | |||||
| public abstract class SettingNames { | |||||
| /* | |||||
| * System-wide settings | |||||
| */ | |||||
| /** Define all available language names as comma separated string */ | |||||
| public static final String SYS_AVAILABLE_LANGUAGES = "SYS.availableLanguages"; | |||||
| /** Define all available locales as comma separated string */ | |||||
| public static final String SYS_AVAILABLE_LOCALES = "SYS.availableLocales"; | |||||
| /** Define the system default locale as string */ | |||||
| public static final String SYS_DEFAULT_LOCALE = "SYS.defaultLocale"; | |||||
| /** Define the system available currencies as comma separated string */ | |||||
| public static final String SYS_CURRENCIES = "SYS.currencies"; | |||||
| /** Define the system modules (authorities.module) */ | |||||
| public static final String SYS_ROLE_MODULES = "SYS.modules"; | |||||
| /* | |||||
| * Mail settings | |||||
| */ | |||||
| /** Mail - SMTP host */ | |||||
| public static final String MAIL_SMTP_HOST = "MAIL.smtp.host"; | |||||
| /** Mail - SMTP port */ | |||||
| public static final String MAIL_SMTP_PORT = "MAIL.smtp.port"; | |||||
| /** Mail - SMTP username */ | |||||
| public static final String MAIL_SMTP_USERNAME = "MAIL.smtp.username"; | |||||
| /** Mail - SMTP password */ | |||||
| public static final String MAIL_SMTP_PASSWORD = "MAIL.smtp.password"; | |||||
| public static final String MAIL_SMTP_RECIPIENTS = "MAIL.smtp.recipients"; | |||||
| public static final String JS_VERSION = "JS.version"; | |||||
| public static final String REPORT_DAILYMAINT_RECIPIENTS_MECH = "REPORT.dailyMaint.recipients.mech"; | |||||
| public static final String REPORT_DAILYMAINT_RECIPIENTS_VOGUE = "REPORT.dailyMaint.recipients.vogue"; | |||||
| public static final String REPORT_DAILYMAINT_RECIPIENTS_VOGUE_CC = "REPORT.dailyMaint.recipients.vogue.cc"; | |||||
| public static final String SYS_PASSWORD_RULE_MIN = "SYS.password.rule.length.min"; | |||||
| public static final String SYS_PASSWORD_RULE_MAX = "SYS.password.rule.length.max"; | |||||
| public static final String SYS_PASSWORD_RULE_NUMBER = "SYS.password.rule.number"; | |||||
| public static final String SYS_PASSWORD_RULE_UPPER_ENG = "SYS.password.rule.upper.eng"; | |||||
| public static final String SYS_PASSWORD_RULE_LOWER_ENG = "SYS.password.rule.lower.eng"; | |||||
| public static final String SYS_PASSWORD_RULE_SPECIAL = "SYS.password.rule.special"; | |||||
| public static final String AUTO_SCHEDULE_MAX_SCHEDULE_DATE = "AUTO_SCHEDULE.maxScheduleDate"; | |||||
| /** PM_CHECKLIST - vogue's signature */ | |||||
| public static final String PM_CHECKLIST_USER_SIGN_ID = "PM_CHECKLIST.vogueSign"; | |||||
| public static final String LCTS_FLOOR = "LCTS.floor"; | |||||
| } | |||||
| @@ -0,0 +1,48 @@ | |||||
| package com.ffii.tsms.modules.common.service; | |||||
| import java.util.Date; | |||||
| import java.util.List; | |||||
| import java.util.Map; | |||||
| import org.springframework.stereotype.Service; | |||||
| import org.springframework.transaction.annotation.Isolation; | |||||
| import org.springframework.transaction.annotation.Transactional; | |||||
| import com.ffii.core.support.AbstractService; | |||||
| import com.ffii.core.support.JdbcDao; | |||||
| import com.ffii.core.utils.MapUtils; | |||||
| import jakarta.annotation.Nullable; | |||||
| @Service | |||||
| public class AuditLogService extends AbstractService { | |||||
| public AuditLogService(JdbcDao jdbcDao) { | |||||
| super(jdbcDao); | |||||
| } | |||||
| private static final String SQL_INSERT_AUDIT_LOG = "INSERT INTO audit_log (`tableName`, `recordId`, `modifiedBy`, `modified`, `oldData`, `newData`) " | |||||
| + "VALUES (:tableName, :recordId, :modifiedBy, :modified, :oldData, :newData)"; | |||||
| @Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = Exception.class, readOnly = false) | |||||
| public int save(String tableName, Long recordId, Long modifiedBy, Date modified, @Nullable String oldData, String newData) { | |||||
| return jdbcDao.executeUpdate(SQL_INSERT_AUDIT_LOG,MapUtils.toHashMap("tableName", tableName, "recordId", recordId, | |||||
| "modifiedBy", modifiedBy, "modified", modified, | |||||
| "oldData", oldData, "newData", newData)); | |||||
| } | |||||
| @Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class, readOnly = true) | |||||
| public List<Map<String, Object>> search(String tableName, Integer recordId) { | |||||
| String sql = "SELECT * FROM audit_log WHERE tableName = :tableName AND recordId = :recordId ORDER BY modified"; | |||||
| return jdbcDao.queryForList(sql, Map.of("tableName", tableName, "recordId", recordId)); | |||||
| } | |||||
| @Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class, readOnly = true) | |||||
| public List<Map<String, Object>> getTables() { | |||||
| String sql = "SELECT DISTINCT tableName FROM audit_log"; | |||||
| return jdbcDao.queryForList(sql, ""); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,74 @@ | |||||
| package com.ffii.tsms.modules.settings.entity; | |||||
| import com.ffii.core.entity.IdEntity; | |||||
| import jakarta.persistence.Column; | |||||
| import jakarta.persistence.Entity; | |||||
| import jakarta.persistence.Table; | |||||
| import jakarta.validation.constraints.NotNull; | |||||
| @Entity | |||||
| @Table(name = "settings") | |||||
| public class Settings extends IdEntity<Long> { | |||||
| public static String TYPE_STRING = "string"; | |||||
| public static String TYPE_INT = "integer"; | |||||
| public static String TYPE_FLOAT = "float"; | |||||
| public static String TYPE_BOOLEAN = "boolean"; | |||||
| public static String TYPE_DATE = "date"; | |||||
| public static String TYPE_TIME = "time"; | |||||
| public static String TYPE_DATETIME = "datetime"; | |||||
| // other "A/B" value must "A" or "B" | |||||
| //lowercase | |||||
| public static String VALUE_BOOLEAN_TRUE = "true"; | |||||
| public static String VALUE_BOOLEAN_FALSE = "false"; | |||||
| // TODO: pattern?? | |||||
| @NotNull | |||||
| @Column | |||||
| private String name; | |||||
| @NotNull | |||||
| @Column | |||||
| private String value; | |||||
| @Column | |||||
| private String category; | |||||
| @Column | |||||
| private String type; | |||||
| public String getName() { | |||||
| return name; | |||||
| } | |||||
| public void setName(String name) { | |||||
| this.name = name; | |||||
| } | |||||
| public String getValue() { | |||||
| return value; | |||||
| } | |||||
| public void setValue(String value) { | |||||
| this.value = value; | |||||
| } | |||||
| public String getCategory() { | |||||
| return category; | |||||
| } | |||||
| public void setCategory(String category) { | |||||
| this.category = category; | |||||
| } | |||||
| public String getType() { | |||||
| return type; | |||||
| } | |||||
| public void setType(String type) { | |||||
| this.type = type; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,12 @@ | |||||
| package com.ffii.tsms.modules.settings.entity; | |||||
| import java.util.Optional; | |||||
| import org.springframework.data.repository.query.Param; | |||||
| import com.ffii.core.support.AbstractRepository; | |||||
| public interface SettingsRepository extends AbstractRepository<Settings, Long> { | |||||
| Optional<Settings> findByName(@Param("name") String name); | |||||
| } | |||||
| @@ -0,0 +1,208 @@ | |||||
| package com.ffii.tsms.modules.settings.service; | |||||
| import java.time.LocalDate; | |||||
| import java.time.LocalDateTime; | |||||
| import java.time.LocalTime; | |||||
| import java.time.format.DateTimeFormatter; | |||||
| import java.time.format.DateTimeParseException; | |||||
| import java.util.Optional; | |||||
| import org.apache.commons.lang3.StringUtils; | |||||
| import org.springframework.stereotype.Service; | |||||
| import org.springframework.transaction.annotation.Transactional; | |||||
| import com.ffii.core.exception.InternalServerErrorException; | |||||
| import com.ffii.core.support.AbstractIdEntityService; | |||||
| import com.ffii.core.support.JdbcDao; | |||||
| import com.ffii.tsms.modules.settings.entity.Settings; | |||||
| import com.ffii.tsms.modules.settings.entity.SettingsRepository; | |||||
| @Service | |||||
| public class SettingsService extends AbstractIdEntityService<Settings, Long, SettingsRepository> { | |||||
| public SettingsService(JdbcDao jdbcDao, SettingsRepository repository) { | |||||
| super(jdbcDao, repository); | |||||
| } | |||||
| public Optional<Settings> findByName(String name) { | |||||
| return this.repository.findByName(name); | |||||
| } | |||||
| public boolean validateType(String type, String value) { | |||||
| if (StringUtils.isBlank(type)) return true; | |||||
| if (Settings.TYPE_STRING.equals(type)) return true; | |||||
| if (Settings.TYPE_BOOLEAN.equals(type)) { | |||||
| return Settings.VALUE_BOOLEAN_TRUE.equals(value) || Settings.VALUE_BOOLEAN_FALSE.equals(value); | |||||
| } | |||||
| if (Settings.TYPE_INT.equals(type)) { | |||||
| try { | |||||
| Integer.parseInt(value); | |||||
| return true; | |||||
| } catch (NumberFormatException e) { | |||||
| return false; | |||||
| } | |||||
| } | |||||
| if (Settings.TYPE_FLOAT.equals(type)) { | |||||
| try { | |||||
| Float.parseFloat(value); | |||||
| return true; | |||||
| } catch (NumberFormatException e) { | |||||
| return false; | |||||
| } | |||||
| } | |||||
| if (Settings.TYPE_DATE.equals(type)) { | |||||
| try { | |||||
| LocalDate.parse(value, DateTimeFormatter.ISO_DATE); | |||||
| return true; | |||||
| } catch (DateTimeParseException e) { | |||||
| return false; | |||||
| } | |||||
| } | |||||
| if (Settings.TYPE_TIME.equals(type)) { | |||||
| try { | |||||
| LocalTime.parse(value, DateTimeFormatter.ISO_TIME); | |||||
| return true; | |||||
| } catch (DateTimeParseException e) { | |||||
| return false; | |||||
| } | |||||
| } | |||||
| if (Settings.TYPE_DATETIME.equals(type)) { | |||||
| try { | |||||
| LocalDateTime.parse(value, DateTimeFormatter.ISO_DATE_TIME); | |||||
| return true; | |||||
| } catch (DateTimeParseException e) { | |||||
| return false; | |||||
| } | |||||
| } | |||||
| if (StringUtils.indexOf(type, "/") >= 0) { | |||||
| for (String t : type.split("/")) { | |||||
| if (t.equals(value)) return true; | |||||
| } | |||||
| return false; | |||||
| } | |||||
| return false; | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public void update(String name, String value) { | |||||
| Settings settings = this.findByName(name) | |||||
| .orElseThrow(InternalServerErrorException::new); | |||||
| if (!validateType(settings.getType(), value)) { | |||||
| throw new InternalServerErrorException(); | |||||
| } | |||||
| settings.setValue(value); | |||||
| this.save(settings); | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public void update(String name, LocalDate date) { | |||||
| this.update(name, date.format(DateTimeFormatter.ISO_DATE)); | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public void update(String name, LocalDateTime datetime) { | |||||
| this.update(name, datetime.format(DateTimeFormatter.ISO_DATE_TIME)); | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public void update(String name, LocalTime time) { | |||||
| this.update(name, time.format(DateTimeFormatter.ISO_TIME)); | |||||
| } | |||||
| public String getString(String name) { | |||||
| return this.findByName(name) | |||||
| .map(Settings::getValue) | |||||
| .orElseThrow(InternalServerErrorException::new); | |||||
| } | |||||
| public int getInt(String name) { | |||||
| return this.findByName(name) | |||||
| .map(Settings::getValue) | |||||
| .map(v -> { | |||||
| try { | |||||
| return Integer.parseInt(v); | |||||
| } catch (final NumberFormatException nfe) { | |||||
| return null; | |||||
| } | |||||
| }) | |||||
| .orElseThrow(InternalServerErrorException::new); | |||||
| } | |||||
| public double getDouble(String name) { | |||||
| return this.findByName(name) | |||||
| .map(Settings::getValue) | |||||
| .map(v -> { | |||||
| try { | |||||
| return Double.parseDouble(v); | |||||
| } catch (final NumberFormatException nfe) { | |||||
| return null; | |||||
| } | |||||
| }) | |||||
| .orElseThrow(InternalServerErrorException::new); | |||||
| } | |||||
| public boolean getBoolean(String name) { | |||||
| return this.findByName(name) | |||||
| .map(Settings::getValue) | |||||
| .map(Settings.VALUE_BOOLEAN_TRUE::equals) | |||||
| .orElseThrow(InternalServerErrorException::new); | |||||
| } | |||||
| public LocalDate getDate(String name) { | |||||
| return this.getDate(name, DateTimeFormatter.ISO_DATE); | |||||
| } | |||||
| private LocalDate getDate(String name, DateTimeFormatter formatter) { | |||||
| return this.findByName(name) | |||||
| .map(Settings::getValue) | |||||
| .map(v -> { | |||||
| try { | |||||
| return LocalDate.parse(v, formatter); | |||||
| } catch (DateTimeParseException e) { | |||||
| return null; | |||||
| } | |||||
| }) | |||||
| .orElseThrow(InternalServerErrorException::new); | |||||
| } | |||||
| public LocalDateTime getDatetime(String name) { | |||||
| return this.getDatetime(name, DateTimeFormatter.ISO_DATE_TIME); | |||||
| } | |||||
| private LocalDateTime getDatetime(String name, DateTimeFormatter formatter) { | |||||
| return this.findByName(name) | |||||
| .map(Settings::getValue) | |||||
| .map(v -> { | |||||
| try { | |||||
| return LocalDateTime.parse(v, formatter); | |||||
| } catch (DateTimeParseException e) { | |||||
| return null; | |||||
| } | |||||
| }) | |||||
| .orElseThrow(InternalServerErrorException::new); | |||||
| } | |||||
| public LocalTime getTime(String name) { | |||||
| return this.getTime(name, DateTimeFormatter.ISO_TIME); | |||||
| } | |||||
| private LocalTime getTime(String name, DateTimeFormatter formatter) { | |||||
| return this.findByName(name) | |||||
| .map(Settings::getValue) | |||||
| .map(v -> { | |||||
| try { | |||||
| return LocalTime.parse(v, formatter); | |||||
| } catch (DateTimeParseException e) { | |||||
| return null; | |||||
| } | |||||
| }) | |||||
| .orElseThrow(InternalServerErrorException::new); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,66 @@ | |||||
| package com.ffii.tsms.modules.settings.web; | |||||
| import java.util.List; | |||||
| import org.springframework.http.HttpStatus; | |||||
| import org.springframework.web.bind.annotation.GetMapping; | |||||
| import org.springframework.web.bind.annotation.PatchMapping; | |||||
| import org.springframework.web.bind.annotation.PathVariable; | |||||
| import org.springframework.web.bind.annotation.RequestBody; | |||||
| import org.springframework.web.bind.annotation.RequestMapping; | |||||
| import org.springframework.web.bind.annotation.ResponseStatus; | |||||
| import org.springframework.web.bind.annotation.RestController; | |||||
| import com.ffii.core.exception.BadRequestException; | |||||
| import com.ffii.core.exception.NotFoundException; | |||||
| import com.ffii.tsms.modules.settings.entity.Settings; | |||||
| import com.ffii.tsms.modules.settings.service.SettingsService; | |||||
| import jakarta.validation.Valid; | |||||
| import jakarta.validation.constraints.NotBlank; | |||||
| @RestController | |||||
| @RequestMapping("/settings") | |||||
| public class SettingsController{ | |||||
| private SettingsService settingsService; | |||||
| public SettingsController(SettingsService settingsService) { | |||||
| this.settingsService = settingsService; | |||||
| } | |||||
| // @Operation(summary = "list system settings") | |||||
| @GetMapping | |||||
| // @PreAuthorize("hasAuthority('ADMIN')") | |||||
| public List<Settings> listAll() { | |||||
| return this.settingsService.listAll(); | |||||
| } | |||||
| // @Operation(summary = "update system setting") | |||||
| @PatchMapping("/{name}") | |||||
| // @PreAuthorize("hasAuthority('ADMIN')") | |||||
| @ResponseStatus(HttpStatus.NO_CONTENT) | |||||
| public void update(@PathVariable String name, @RequestBody @Valid UpdateReq body) { | |||||
| Settings entity = this.settingsService.findByName(name) | |||||
| .orElseThrow(NotFoundException::new); | |||||
| if (!this.settingsService.validateType(entity.getType(), body.value)) { | |||||
| throw new BadRequestException(); | |||||
| } | |||||
| entity.setValue(body.value); | |||||
| this.settingsService.save(entity); | |||||
| } | |||||
| public static class UpdateReq { | |||||
| @NotBlank | |||||
| private String value; | |||||
| public String getValue() { | |||||
| return value; | |||||
| } | |||||
| public void setValue(String value) { | |||||
| this.value = value; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,37 @@ | |||||
| package com.ffii.tsms.modules.user.entity; | |||||
| import jakarta.persistence.Column; | |||||
| import jakarta.persistence.Entity; | |||||
| import jakarta.persistence.Table; | |||||
| import jakarta.validation.constraints.NotNull; | |||||
| import com.ffii.core.entity.BaseEntity; | |||||
| @Entity | |||||
| @Table(name = "`group`") | |||||
| public class Group extends BaseEntity<Long> { | |||||
| @NotNull | |||||
| @Column | |||||
| private String name; | |||||
| @Column | |||||
| private String description; | |||||
| public String getName() { | |||||
| return name; | |||||
| } | |||||
| public void setName(String name) { | |||||
| this.name = name; | |||||
| } | |||||
| public String getDescription() { | |||||
| return description; | |||||
| } | |||||
| public void setDescription(String description) { | |||||
| this.description = description; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,6 @@ | |||||
| package com.ffii.tsms.modules.user.entity; | |||||
| import com.ffii.core.support.AbstractRepository; | |||||
| public interface GroupRepository extends AbstractRepository<Group, Long> { | |||||
| } | |||||
| @@ -0,0 +1,254 @@ | |||||
| package com.ffii.tsms.modules.user.entity; | |||||
| import java.time.LocalDate; | |||||
| import java.util.Collection; | |||||
| import jakarta.persistence.Column; | |||||
| import jakarta.persistence.Entity; | |||||
| import jakarta.persistence.Table; | |||||
| import jakarta.persistence.Transient; | |||||
| import jakarta.validation.constraints.NotBlank; | |||||
| import jakarta.validation.constraints.NotNull; | |||||
| import org.springframework.security.core.GrantedAuthority; | |||||
| import org.springframework.security.core.userdetails.UserDetails; | |||||
| import com.fasterxml.jackson.annotation.JsonIgnore; | |||||
| import com.ffii.core.entity.BaseEntity; | |||||
| /** @author Terence */ | |||||
| @Entity | |||||
| @Table(name = "user") | |||||
| public class User extends BaseEntity<Long> implements UserDetails { | |||||
| @NotBlank | |||||
| @Column(unique = true) | |||||
| private String username; | |||||
| @JsonIgnore | |||||
| @NotBlank | |||||
| @Column | |||||
| private String password; | |||||
| // @NotNull | |||||
| @Column | |||||
| private Boolean locked = Boolean.FALSE; | |||||
| @NotBlank | |||||
| @Column | |||||
| private String name; | |||||
| @Column | |||||
| private LocalDate expiryDate; | |||||
| @JsonIgnore | |||||
| @Transient | |||||
| private Collection<GrantedAuthority> authorities; | |||||
| @Column | |||||
| private String locale; | |||||
| @Column | |||||
| private String fullname; | |||||
| @Column | |||||
| private String firstname; | |||||
| @Column | |||||
| private String lastname; | |||||
| @Column | |||||
| private String department; | |||||
| @Column | |||||
| private String title; | |||||
| @Column | |||||
| private String email; | |||||
| @Column | |||||
| private String phone1; | |||||
| @Column | |||||
| private String phone2; | |||||
| @Column | |||||
| private String remarks; | |||||
| @Column | |||||
| private boolean lotusNotesUser = false; | |||||
| public boolean isLocked() { | |||||
| return this.locked == null ? false : this.locked; | |||||
| } | |||||
| // getter & setter | |||||
| public void setUsername(String username) { | |||||
| this.username = username; | |||||
| } | |||||
| public void setPassword(String password) { | |||||
| this.password = password; | |||||
| } | |||||
| public Boolean getLocked() { | |||||
| return locked; | |||||
| } | |||||
| public void setLocked(Boolean locked) { | |||||
| this.locked = locked; | |||||
| } | |||||
| public String getName() { | |||||
| return name; | |||||
| } | |||||
| public void setName(String name) { | |||||
| this.name = name; | |||||
| } | |||||
| public LocalDate getExpiryDate() { | |||||
| return expiryDate; | |||||
| } | |||||
| public void setExpiryDate(LocalDate expiryDate) { | |||||
| this.expiryDate = expiryDate; | |||||
| } | |||||
| public void setAuthorities(Collection<GrantedAuthority> authorities) { | |||||
| this.authorities = authorities; | |||||
| } | |||||
| public String getLocale() { | |||||
| return locale; | |||||
| } | |||||
| public void setLocale(String locale) { | |||||
| this.locale = locale; | |||||
| } | |||||
| public String getFullname() { | |||||
| return fullname; | |||||
| } | |||||
| public void setFullname(String fullname) { | |||||
| this.fullname = fullname; | |||||
| } | |||||
| public String getFirstname() { | |||||
| return firstname; | |||||
| } | |||||
| public void setFirstname(String firstname) { | |||||
| this.firstname = firstname; | |||||
| } | |||||
| public String getLastname() { | |||||
| return lastname; | |||||
| } | |||||
| public void setLastname(String lastname) { | |||||
| this.lastname = lastname; | |||||
| } | |||||
| public String getTitle() { | |||||
| return title; | |||||
| } | |||||
| public void setTitle(String title) { | |||||
| this.title = title; | |||||
| } | |||||
| public String getEmail() { | |||||
| return email; | |||||
| } | |||||
| public void setEmail(String email) { | |||||
| this.email = email; | |||||
| } | |||||
| public String getPhone1() { | |||||
| return phone1; | |||||
| } | |||||
| public void setPhone1(String phone1) { | |||||
| this.phone1 = phone1; | |||||
| } | |||||
| public String getPhone2() { | |||||
| return phone2; | |||||
| } | |||||
| public void setPhone2(String phone2) { | |||||
| this.phone2 = phone2; | |||||
| } | |||||
| public String getRemarks() { | |||||
| return remarks; | |||||
| } | |||||
| public void setRemarks(String remarks) { | |||||
| this.remarks = remarks; | |||||
| } | |||||
| // override | |||||
| @Override | |||||
| public Collection<? extends GrantedAuthority> getAuthorities() { | |||||
| return this.authorities; | |||||
| } | |||||
| @Override | |||||
| public String getPassword() { | |||||
| return this.password; | |||||
| } | |||||
| @Override | |||||
| public String getUsername() { | |||||
| return this.username; | |||||
| } | |||||
| @Override | |||||
| public boolean isAccountNonExpired() { | |||||
| return this.getExpiryDate() == null || this.getExpiryDate().isAfter(LocalDate.now()); | |||||
| } | |||||
| @Override | |||||
| public boolean isAccountNonLocked() { | |||||
| return !this.isLocked(); | |||||
| } | |||||
| @JsonIgnore | |||||
| @Override | |||||
| public boolean isCredentialsNonExpired() { | |||||
| return true; | |||||
| } | |||||
| @JsonIgnore | |||||
| @Override | |||||
| public boolean isEnabled() { | |||||
| return true; | |||||
| } | |||||
| public String getDepartment() { | |||||
| return department; | |||||
| } | |||||
| public void setDepartment(String department) { | |||||
| this.department = department; | |||||
| } | |||||
| public boolean isLotusNotesUser() { | |||||
| return this.lotusNotesUser; | |||||
| } | |||||
| public boolean getLotusNotesUser() { | |||||
| return this.lotusNotesUser; | |||||
| } | |||||
| public void setLotusNotesUser(boolean lotusNotesUser) { | |||||
| this.lotusNotesUser = lotusNotesUser; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,15 @@ | |||||
| package com.ffii.tsms.modules.user.entity; | |||||
| import java.util.List; | |||||
| import java.util.Optional; | |||||
| import org.springframework.data.repository.query.Param; | |||||
| import com.ffii.core.support.AbstractRepository; | |||||
| public interface UserRepository extends AbstractRepository<User, Long> { | |||||
| List<User> findByName(@Param("name") String name); | |||||
| Optional<User> findByUsernameAndDeletedFalse(String username); | |||||
| } | |||||
| @@ -0,0 +1,29 @@ | |||||
| package com.ffii.tsms.modules.user.req; | |||||
| import jakarta.validation.constraints.Pattern; | |||||
| import jakarta.validation.constraints.Size; | |||||
| /** @author Alex */ | |||||
| public class NewPublicUserReq extends UpdateUserReq { | |||||
| @Size(max = 30) | |||||
| @Pattern(regexp = "^[A-Za-z0-9]+$") | |||||
| private String username; | |||||
| private String password; | |||||
| public String getUsername() { | |||||
| return username; | |||||
| } | |||||
| public void setUsername(String username) { | |||||
| this.username = username; | |||||
| } | |||||
| public String getPassword() { | |||||
| return this.password; | |||||
| } | |||||
| public void setPassword(String password) { | |||||
| this.password = password; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,21 @@ | |||||
| package com.ffii.tsms.modules.user.req; | |||||
| import jakarta.validation.constraints.Pattern; | |||||
| import jakarta.validation.constraints.Size; | |||||
| /** @author Alex */ | |||||
| public class NewUserReq extends UpdateUserReq { | |||||
| @Size(max = 30) | |||||
| @Pattern(regexp = "^[A-Za-z0-9]+$") | |||||
| private String username; | |||||
| public String getUsername() { | |||||
| return username; | |||||
| } | |||||
| public void setUsername(String username) { | |||||
| this.username = username; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,80 @@ | |||||
| package com.ffii.tsms.modules.user.req; | |||||
| import java.util.List; | |||||
| import jakarta.validation.constraints.NotNull; | |||||
| public class SaveGroupReq { | |||||
| private Long id; | |||||
| @NotNull | |||||
| private String name; | |||||
| private String description; | |||||
| @NotNull | |||||
| private List<Long> addUserIds; | |||||
| @NotNull | |||||
| private List<Long> removeUserIds; | |||||
| @NotNull | |||||
| private List<Long> addAuthIds; | |||||
| @NotNull | |||||
| private List<Long> removeAuthIds; | |||||
| public Long getId() { | |||||
| return id; | |||||
| } | |||||
| public void setId(Long id) { | |||||
| this.id = id; | |||||
| } | |||||
| public String getName() { | |||||
| return name; | |||||
| } | |||||
| public void setName(String name) { | |||||
| this.name = name; | |||||
| } | |||||
| public String getDescription() { | |||||
| return description; | |||||
| } | |||||
| public void setDescription(String description) { | |||||
| this.description = description; | |||||
| } | |||||
| public List<Long> getAddUserIds() { | |||||
| return addUserIds; | |||||
| } | |||||
| public void setAddUserIds(List<Long> addUserIds) { | |||||
| this.addUserIds = addUserIds; | |||||
| } | |||||
| public List<Long> getRemoveUserIds() { | |||||
| return removeUserIds; | |||||
| } | |||||
| public void setRemoveUserIds(List<Long> removeUserIds) { | |||||
| this.removeUserIds = removeUserIds; | |||||
| } | |||||
| public List<Long> getAddAuthIds() { | |||||
| return addAuthIds; | |||||
| } | |||||
| public void setAddAuthIds(List<Long> addAuthIds) { | |||||
| this.addAuthIds = addAuthIds; | |||||
| } | |||||
| public List<Long> getRemoveAuthIds() { | |||||
| return removeAuthIds; | |||||
| } | |||||
| public void setRemoveAuthIds(List<Long> removeAuthIds) { | |||||
| this.removeAuthIds = removeAuthIds; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,69 @@ | |||||
| package com.ffii.tsms.modules.user.req; | |||||
| public class SearchUserReq { | |||||
| private Integer id; | |||||
| private Integer groupId; | |||||
| private String username; | |||||
| private String name; | |||||
| private Boolean locked; | |||||
| private Integer start; | |||||
| private Integer limit; | |||||
| public Integer getId() { | |||||
| return id; | |||||
| } | |||||
| public void setId(Integer id) { | |||||
| this.id = id; | |||||
| } | |||||
| public Integer getGroupId() { | |||||
| return groupId; | |||||
| } | |||||
| public void setGroupId(Integer groupId) { | |||||
| this.groupId = groupId; | |||||
| } | |||||
| public String getUsername() { | |||||
| return username; | |||||
| } | |||||
| public void setUsername(String username) { | |||||
| this.username = username; | |||||
| } | |||||
| public String getName() { | |||||
| return name; | |||||
| } | |||||
| public void setName(String name) { | |||||
| this.name = name; | |||||
| } | |||||
| public Integer getStart() { | |||||
| return start; | |||||
| } | |||||
| public void setStart(Integer start) { | |||||
| this.start = start; | |||||
| } | |||||
| public Integer getLimit() { | |||||
| return limit; | |||||
| } | |||||
| public void setLimit(Integer limit) { | |||||
| this.limit = limit; | |||||
| } | |||||
| public Boolean getLocked() { | |||||
| return locked; | |||||
| } | |||||
| public void setLocked(Boolean locked) { | |||||
| this.locked = locked; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,151 @@ | |||||
| package com.ffii.tsms.modules.user.req; | |||||
| import java.time.LocalDate; | |||||
| import java.util.List; | |||||
| import jakarta.validation.constraints.Email; | |||||
| import jakarta.validation.constraints.NotBlank; | |||||
| import jakarta.validation.constraints.NotNull; | |||||
| import jakarta.validation.constraints.Size; | |||||
| /** @author Alex */ | |||||
| public class UpdateUserReq { | |||||
| @NotNull | |||||
| private Boolean locked; | |||||
| @Size(max = 90) | |||||
| @NotBlank | |||||
| private String name; | |||||
| private String firstname; | |||||
| private String lastname; | |||||
| private LocalDate expiryDate; | |||||
| private String locale; | |||||
| private String remarks; | |||||
| @NotBlank | |||||
| private String email; | |||||
| @NotBlank | |||||
| private String department; | |||||
| // @NotNull | |||||
| private List<Integer> addGroupIds; | |||||
| // @NotNull | |||||
| private List<Integer> removeGroupIds; | |||||
| // @NotNull | |||||
| private List<Integer> addAuthIds; | |||||
| // @NotNull | |||||
| private List<Integer> removeAuthIds; | |||||
| public Boolean getLocked() { | |||||
| return locked; | |||||
| } | |||||
| public void setLocked(Boolean locked) { | |||||
| this.locked = locked; | |||||
| } | |||||
| public String getName() { | |||||
| return name; | |||||
| } | |||||
| public void setName(String name) { | |||||
| this.name = name; | |||||
| } | |||||
| public LocalDate getExpiryDate() { | |||||
| return expiryDate; | |||||
| } | |||||
| public void setExpiryDate(LocalDate expiryDate) { | |||||
| this.expiryDate = expiryDate; | |||||
| } | |||||
| public String getFirstname() { | |||||
| return firstname; | |||||
| } | |||||
| public void setFirstName(String firstname) { | |||||
| this.firstname = firstname; | |||||
| } | |||||
| public String getLastname() { | |||||
| return lastname; | |||||
| } | |||||
| public void setLastname(String lastname) { | |||||
| this.lastname = lastname; | |||||
| } | |||||
| public String getLocale() { | |||||
| return locale; | |||||
| } | |||||
| public void setLocale(String locale) { | |||||
| this.locale = locale; | |||||
| } | |||||
| public void setFirstname(String firstname) { | |||||
| this.firstname = firstname; | |||||
| } | |||||
| public List<Integer> getAddGroupIds() { | |||||
| return addGroupIds; | |||||
| } | |||||
| public void setAddGroupIds(List<Integer> addGroupIds) { | |||||
| this.addGroupIds = addGroupIds; | |||||
| } | |||||
| public List<Integer> getRemoveGroupIds() { | |||||
| return removeGroupIds; | |||||
| } | |||||
| public void setRemoveGroupIds(List<Integer> removeGroupIds) { | |||||
| this.removeGroupIds = removeGroupIds; | |||||
| } | |||||
| public List<Integer> getAddAuthIds() { | |||||
| return addAuthIds; | |||||
| } | |||||
| public void setAddAuthIds(List<Integer> addAuthIds) { | |||||
| this.addAuthIds = addAuthIds; | |||||
| } | |||||
| public List<Integer> getRemoveAuthIds() { | |||||
| return removeAuthIds; | |||||
| } | |||||
| public void setRemoveAuthIds(List<Integer> removeAuthIds) { | |||||
| this.removeAuthIds = removeAuthIds; | |||||
| } | |||||
| public String getRemarks() { | |||||
| return remarks; | |||||
| } | |||||
| public void setRemarks(String remarks) { | |||||
| this.remarks = remarks; | |||||
| } | |||||
| public String getEmail() { | |||||
| return email; | |||||
| } | |||||
| public void setEmail(String email) { | |||||
| this.email = email; | |||||
| } | |||||
| public String getDepartment() { | |||||
| return department; | |||||
| } | |||||
| public void setDepartment(String department) { | |||||
| this.department = department; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,176 @@ | |||||
| package com.ffii.tsms.modules.user.service; | |||||
| import java.util.Date; | |||||
| import java.util.List; | |||||
| import java.util.Map; | |||||
| import java.util.stream.Collectors; | |||||
| import org.springframework.beans.BeanUtils; | |||||
| import org.springframework.beans.factory.annotation.Autowired; | |||||
| import org.springframework.stereotype.Service; | |||||
| import org.springframework.transaction.annotation.Transactional; | |||||
| import com.ffii.core.exception.NotFoundException; | |||||
| import com.ffii.core.support.AbstractBaseEntityService; | |||||
| import com.ffii.core.support.JdbcDao; | |||||
| import com.ffii.core.utils.JsonUtils; | |||||
| import com.ffii.core.utils.Params; | |||||
| import com.ffii.tsms.modules.common.SecurityUtils; | |||||
| import com.ffii.tsms.modules.common.service.AuditLogService; | |||||
| import com.ffii.tsms.modules.user.entity.Group; | |||||
| import com.ffii.tsms.modules.user.entity.GroupRepository; | |||||
| import com.ffii.tsms.modules.user.req.SaveGroupReq; | |||||
| import jakarta.persistence.Table; | |||||
| import jakarta.validation.Valid; | |||||
| @Service | |||||
| public class GroupService extends AbstractBaseEntityService<Group, Long, GroupRepository> { | |||||
| @Autowired | |||||
| private AuditLogService auditLogService; | |||||
| public GroupService(JdbcDao jdbcDao, GroupRepository repository) { | |||||
| super(jdbcDao, repository); | |||||
| } | |||||
| public List<Map<String, Object>> search(Map<String, Object> args) { | |||||
| StringBuilder sql = new StringBuilder("SELECT" | |||||
| + " g.*" | |||||
| + " FROM `group` g" | |||||
| + " WHERE g.deleted = FALSE"); | |||||
| if (args != null) { | |||||
| if (args.containsKey(Params.QUERY)) sql.append(" AND (g.name LIKE :query)"); | |||||
| if (args.containsKey(Params.ID)) sql.append(" AND g.id = :id"); | |||||
| if (args.containsKey(Params.NAME)) sql.append(" AND g.name LIKE :name"); | |||||
| } | |||||
| sql.append(" ORDER BY g.name"); | |||||
| return jdbcDao.queryForList(sql.toString(), args); | |||||
| } | |||||
| public List<Map<String, Object>> searchForCombo(Map<String, Object> args) { | |||||
| StringBuilder sql = new StringBuilder("SELECT" | |||||
| + " g.id," | |||||
| + " g.name" | |||||
| + " FROM `group` g" | |||||
| + " WHERE g.deleted = FALSE"); | |||||
| if (args != null) { | |||||
| if (args.containsKey(Params.QUERY)) sql.append(" AND (g.name LIKE :query)"); | |||||
| if (args.containsKey(Params.ID)) sql.append(" AND g.id = :id"); | |||||
| } | |||||
| sql.append(" ORDER BY g.name"); | |||||
| return jdbcDao.queryForList(sql.toString(), args); | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public void delete(Group instance) { | |||||
| Map<String, Object> args = Map.of("groupId", instance.getId()); | |||||
| jdbcDao.executeUpdate("DELETE FROM user_group WHERE groupId = :groupId;", args); | |||||
| jdbcDao.executeUpdate("DELETE FROM group_authority WHERE groupId = :groupId;", args); | |||||
| markDelete(instance); | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public Group saveOrUpdate(@Valid SaveGroupReq req ) { | |||||
| Group instance; | |||||
| if (req.getId() != null) { | |||||
| instance = find(req.getId()).orElseThrow(NotFoundException::new); | |||||
| } else { | |||||
| instance = new Group(); | |||||
| } | |||||
| BeanUtils.copyProperties(req, instance); | |||||
| String tableName = instance.getClass().getAnnotation(Table.class).name(); | |||||
| StringBuilder sql = new StringBuilder("SELECT * FROM " + tableName + " WHERE id = :id"); | |||||
| String oldValueJson = null; | |||||
| String newValueJson = null; | |||||
| if (instance != null && instance.getId() != null && instance.getId() > 0) { | |||||
| oldValueJson = JsonUtils.toJsonString(jdbcDao.queryForMap(sql.toString(), Map.of("id", instance.getId())).orElseThrow(NotFoundException::new)); | |||||
| } | |||||
| instance = saveAndFlush(instance); | |||||
| Long id = instance.getId(); | |||||
| List<Map<String, Long>> userBatchInsertValues = req.getAddUserIds().stream() | |||||
| .map(userId -> Map.of("groupId", id, "userId", userId)) | |||||
| .collect(Collectors.toList()); | |||||
| List<Map<String, Long>> userBatchDeleteValues = req.getRemoveUserIds().stream() | |||||
| .map(userId -> Map.of("groupId", id, "userId", userId)) | |||||
| .collect(Collectors.toList()); | |||||
| if (!userBatchInsertValues.isEmpty()) { | |||||
| jdbcDao.batchUpdate( | |||||
| "INSERT IGNORE INTO user_group (groupId,userId)" | |||||
| + " VALUES (:groupId, :userId)", | |||||
| userBatchInsertValues); | |||||
| } | |||||
| if (!userBatchDeleteValues.isEmpty()) { | |||||
| jdbcDao.batchUpdate( | |||||
| "DELETE FROM user_group" | |||||
| + " WHERE groupId = :groupId AND userId = :userId", | |||||
| userBatchDeleteValues); | |||||
| } | |||||
| List<Map<String, Long>> authBatchInsertValues = req.getAddAuthIds().stream() | |||||
| .map(authId -> Map.of("groupId", id, "authId", authId)) | |||||
| .collect(Collectors.toList()); | |||||
| List<Map<String, Long>> authBatchDeleteValues = req.getRemoveAuthIds().stream() | |||||
| .map(authId -> Map.of("groupId", id, "authId", authId)) | |||||
| .collect(Collectors.toList()); | |||||
| if (!authBatchInsertValues.isEmpty()) { | |||||
| jdbcDao.batchUpdate( | |||||
| "INSERT IGNORE INTO group_authority (groupId, authId)" | |||||
| + " VALUES (:groupId, :authId)", | |||||
| authBatchInsertValues); | |||||
| } | |||||
| if (!authBatchDeleteValues.isEmpty()) { | |||||
| jdbcDao.batchUpdate( | |||||
| "DELETE FROM group_authority" | |||||
| + " WHERE groupId = :groupId AND authId = :authId", | |||||
| authBatchDeleteValues); | |||||
| } | |||||
| if (instance != null && instance.getId() != null && instance.getId() > 0) { | |||||
| newValueJson = JsonUtils.toJsonString(jdbcDao.queryForMap(sql.toString(), Map.of("id", instance.getId())).orElseThrow(NotFoundException::new)); | |||||
| } | |||||
| auditLogService.save( | |||||
| tableName, | |||||
| id, | |||||
| SecurityUtils.getUser() != null ? SecurityUtils.getUser().get().getId() : null, | |||||
| new Date(), | |||||
| oldValueJson, | |||||
| newValueJson); | |||||
| return instance; | |||||
| } | |||||
| public List<Integer> listGroupAuthId(Long id) { | |||||
| return jdbcDao.queryForInts( | |||||
| "SELECT" | |||||
| + " ga.authId" | |||||
| + " FROM group_authority ga" | |||||
| + " WHERE ga.groupId = :id", | |||||
| Map.of(Params.ID, id)); | |||||
| } | |||||
| public List<Integer> listGroupUserId(Long id) { | |||||
| return jdbcDao.queryForInts( | |||||
| "SELECT" | |||||
| + " gu.userId" | |||||
| + " FROM user_group gu" | |||||
| + " INNER JOIN user u ON u.deleted = FALSE AND gu.userId = u.id" | |||||
| + " WHERE gu.groupId = :id", | |||||
| Map.of(Params.ID, id)); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,48 @@ | |||||
| package com.ffii.tsms.modules.user.service; | |||||
| import java.util.HashSet; | |||||
| import java.util.List; | |||||
| import java.util.Map; | |||||
| import java.util.Set; | |||||
| import org.springframework.security.core.authority.SimpleGrantedAuthority; | |||||
| import org.springframework.stereotype.Service; | |||||
| import org.springframework.transaction.annotation.Transactional; | |||||
| import com.ffii.core.support.AbstractService; | |||||
| import com.ffii.core.support.JdbcDao; | |||||
| import com.ffii.tsms.modules.user.entity.User; | |||||
| @Service | |||||
| public class UserAuthorityService extends AbstractService { | |||||
| private static final String USER_AUTH_SQL = "SELECT a.authority" | |||||
| + " FROM `user` u" | |||||
| + " JOIN user_authority ua ON ua.userId = u.id" | |||||
| + " JOIN authority a ON a.id = ua.authId" | |||||
| + " WHERE u.deleted = 0" | |||||
| + " AND u.id = :userId"; | |||||
| private static final String UNION_SQL = " UNION "; | |||||
| private static final String GROUP_AUTH_SQL = "SELECT a.authority" | |||||
| + " FROM `user` u" | |||||
| + " JOIN user_group ug ON ug.userId = u.id" | |||||
| + " JOIN `group` g ON g.deleted = 0 AND g.id = ug.groupId" | |||||
| + " JOIN group_authority ga ON ga.groupId = g.id" | |||||
| + " JOIN authority a ON a.id = ga.authId" | |||||
| + " WHERE u.deleted = 0" | |||||
| + " AND u.id = :userId"; | |||||
| public UserAuthorityService(JdbcDao jdbcDao) { | |||||
| super(jdbcDao); | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public Set<SimpleGrantedAuthority> getUserAuthority(User user) { | |||||
| Set<SimpleGrantedAuthority> auths = new HashSet<>(); | |||||
| List<Map<String, Object>> records = jdbcDao.queryForList(USER_AUTH_SQL + UNION_SQL + GROUP_AUTH_SQL, | |||||
| Map.of("userId", user.getId())); | |||||
| records.forEach(item -> auths.add(new SimpleGrantedAuthority((String) item.get("authority")))); | |||||
| return auths; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,269 @@ | |||||
| package com.ffii.tsms.modules.user.service; | |||||
| import java.io.UnsupportedEncodingException; | |||||
| import java.util.LinkedHashSet; | |||||
| import java.util.List; | |||||
| import java.util.Locale; | |||||
| import java.util.Map; | |||||
| import java.util.Optional; | |||||
| import java.util.Set; | |||||
| import java.util.stream.Collectors; | |||||
| import org.apache.commons.lang3.LocaleUtils; | |||||
| import org.apache.commons.lang3.StringUtils; | |||||
| import org.springframework.beans.BeanUtils; | |||||
| import org.springframework.beans.factory.annotation.Autowired; | |||||
| import org.springframework.security.core.GrantedAuthority; | |||||
| import org.springframework.security.core.authority.SimpleGrantedAuthority; | |||||
| import org.springframework.security.crypto.password.PasswordEncoder; | |||||
| import org.springframework.stereotype.Service; | |||||
| import org.springframework.transaction.annotation.Transactional; | |||||
| import com.ffii.core.exception.NotFoundException; | |||||
| import com.ffii.core.exception.UnprocessableEntityException; | |||||
| import com.ffii.core.support.AbstractBaseEntityService; | |||||
| import com.ffii.core.support.JdbcDao; | |||||
| import com.ffii.core.utils.Params; | |||||
| import com.ffii.core.utils.PasswordUtils; | |||||
| import com.ffii.tsms.modules.common.ErrorCodes; | |||||
| import com.ffii.tsms.modules.common.PasswordRule; | |||||
| import com.ffii.tsms.modules.settings.service.SettingsService; | |||||
| import com.ffii.tsms.modules.user.entity.User; | |||||
| import com.ffii.tsms.modules.user.entity.UserRepository; | |||||
| import com.ffii.tsms.modules.user.req.NewPublicUserReq; | |||||
| import com.ffii.tsms.modules.user.req.NewUserReq; | |||||
| import com.ffii.tsms.modules.user.req.SearchUserReq; | |||||
| import com.ffii.tsms.modules.user.req.UpdateUserReq; | |||||
| import com.ffii.tsms.modules.user.service.pojo.UserRecord; | |||||
| import jakarta.mail.internet.InternetAddress; | |||||
| @Service | |||||
| public class UserService extends AbstractBaseEntityService<User, Long, UserRepository> { | |||||
| private static final String USER_AUTH_SQL = "SELECT a.authority" | |||||
| + " FROM `user` u" | |||||
| + " JOIN user_authority ua ON ua.userId = u.id" | |||||
| + " JOIN authority a ON a.id = ua.authId" | |||||
| + " WHERE u.deleted = 0" | |||||
| + " AND u.id = :userId"; | |||||
| private static final String UNION_SQL = " UNION "; | |||||
| private static final String GROUP_AUTH_SQL = "SELECT a.authority" | |||||
| + " FROM `user` u" | |||||
| + " JOIN user_group ug ON ug.userId = u.id" | |||||
| + " JOIN `group` g ON g.deleted = 0 AND g.id = ug.groupId" | |||||
| + " JOIN group_authority ga ON ga.groupId = g.id" | |||||
| + " JOIN authority a ON a.id = ga.authId" | |||||
| + " WHERE u.deleted = 0" | |||||
| + " AND u.id = :userId"; | |||||
| @Autowired | |||||
| private SettingsService settingsService; | |||||
| @Autowired | |||||
| private PasswordEncoder passwordEncoder; | |||||
| @Autowired | |||||
| UserRepository userRepository; | |||||
| public UserService(JdbcDao jdbcDao, UserRepository userRepository) { | |||||
| super(jdbcDao, userRepository); | |||||
| } | |||||
| public Optional<User> loadUserOptByUsername(String username) { | |||||
| return findByUsername(username) | |||||
| .map(user -> { | |||||
| Set<GrantedAuthority> auths = new LinkedHashSet<GrantedAuthority>(); | |||||
| auths.add(new SimpleGrantedAuthority("ROLE_USER")); | |||||
| jdbcDao.queryForList(USER_AUTH_SQL + UNION_SQL + GROUP_AUTH_SQL, Map.of("userId", user.getId())) | |||||
| .forEach(item -> auths.add(new SimpleGrantedAuthority((String) item.get("authority")))); | |||||
| user.setAuthorities(auths); | |||||
| return user; | |||||
| }); | |||||
| } | |||||
| public Optional<User> findByUsername(String username) { | |||||
| return userRepository.findByUsernameAndDeletedFalse(username); | |||||
| } | |||||
| // @Transactional(rollbackFor = Exception.class) | |||||
| public List<UserRecord> search(SearchUserReq req) { | |||||
| StringBuilder sql = new StringBuilder("SELECT" | |||||
| + " u.id," | |||||
| + " u.created," | |||||
| + " u.createdBy," | |||||
| + " u.version," | |||||
| + " u.modified," | |||||
| + " u.modifiedBy," | |||||
| + " u.username," | |||||
| + " u.locked," | |||||
| + " u.name," | |||||
| + " u.locale," | |||||
| + " u.firstname," | |||||
| + " u.lastname," | |||||
| + " u.title," | |||||
| + " u.department," | |||||
| + " u.email," | |||||
| + " u.phone1," | |||||
| + " u.phone2," | |||||
| + " u.remarks " | |||||
| + " FROM `user` u" | |||||
| + " left join user_group ug on u.id = ug.userId" | |||||
| + " where u.deleted = false"); | |||||
| if (req != null) { | |||||
| if (req.getId() != null) | |||||
| sql.append(" AND u.id = :id"); | |||||
| if (req.getGroupId() != null) | |||||
| sql.append(" AND ug.groupId = :groupId"); | |||||
| if (StringUtils.isNotBlank(req.getUsername())) { | |||||
| req.setUsername("%" + req.getUsername() + "%"); | |||||
| sql.append(" AND u.username LIKE :username"); | |||||
| } | |||||
| if (StringUtils.isNotBlank(req.getName())) { | |||||
| req.setName("%" + req.getName() + "%"); | |||||
| sql.append(" AND u.name LIKE :name"); | |||||
| } | |||||
| if (req.getLocked() != null) { | |||||
| sql.append(" AND u.locked = :locked"); | |||||
| } | |||||
| } | |||||
| sql.append(" ORDER BY u.name"); | |||||
| if (req != null) { | |||||
| if (req.getStart() != null && req.getLimit() != null) | |||||
| sql.append(" LIMIT :start, :limit"); | |||||
| } | |||||
| return jdbcDao.queryForList(sql.toString(), req, UserRecord.class); | |||||
| } | |||||
| public List<Integer> listUserAuthId(long id) { | |||||
| return jdbcDao.queryForInts( | |||||
| "SELECT" | |||||
| + " ua.authId" | |||||
| + " FROM user_authority ua" | |||||
| + " WHERE ua.userId = :id", | |||||
| Map.of(Params.ID, id)); | |||||
| } | |||||
| public List<Integer> listUserGroupId(long id) { | |||||
| return jdbcDao.queryForInts( | |||||
| "SELECT" | |||||
| + " gu.groupId" | |||||
| + " FROM user_group gu" | |||||
| + " INNER JOIN `group` g ON g.deleted = FALSE AND g.id = gu.groupId" | |||||
| + " WHERE gu.userId = :id", | |||||
| Map.of(Params.ID, id)); | |||||
| } | |||||
| private User saveOrUpdate(User instance, UpdateUserReq req) { | |||||
| if (instance.getId() == null){ | |||||
| req.setLocked(false); | |||||
| } | |||||
| BeanUtils.copyProperties(req,instance); | |||||
| instance = save(instance); | |||||
| // long id = instance.getId(); | |||||
| // List<Map<String, Integer>> groupBatchInsertValues = req.getAddGroupIds().stream() | |||||
| // .map(groupId -> Map.of("userId", (int) id, "groupId", groupId)) | |||||
| // .collect(Collectors.toList()); | |||||
| // List<Map<String, Integer>> groupBatchDeleteValues = req.getRemoveGroupIds().stream() | |||||
| // .map(groupId -> Map.of("userId", (int) id, "groupId", groupId)) | |||||
| // .collect(Collectors.toList()); | |||||
| // if (!groupBatchInsertValues.isEmpty()) { | |||||
| // jdbcDao.batchUpdate( | |||||
| // "INSERT IGNORE INTO user_group (groupId,userId)" | |||||
| // + " VALUES (:groupId, :userId)", | |||||
| // groupBatchInsertValues); | |||||
| // } | |||||
| // if (!groupBatchDeleteValues.isEmpty()) { | |||||
| // jdbcDao.batchUpdate( | |||||
| // "DELETE FROM user_group" | |||||
| // + " WHERE groupId = :groupId AND userId = :userId", | |||||
| // groupBatchDeleteValues); | |||||
| // } | |||||
| // List<Map<String, Integer>> authBatchInsertValues = req.getAddAuthIds().stream() | |||||
| // .map(authId -> Map.of("userId", (int)id, "authId", authId)) | |||||
| // .collect(Collectors.toList()); | |||||
| // List<Map<String, Integer>> authBatchDeleteValues = req.getRemoveAuthIds().stream() | |||||
| // .map(authId -> Map.of("userId", (int)id, "authId", authId)) | |||||
| // .collect(Collectors.toList()); | |||||
| // if (!authBatchInsertValues.isEmpty()) { | |||||
| // jdbcDao.batchUpdate( | |||||
| // "INSERT IGNORE INTO user_authority (userId, authId)" | |||||
| // + " VALUES (:userId, :authId)", | |||||
| // authBatchInsertValues); | |||||
| // } | |||||
| // if (!authBatchDeleteValues.isEmpty()) { | |||||
| // jdbcDao.batchUpdate( | |||||
| // "DELETE FROM user_authority" | |||||
| // + " WHERE userId = :userId AND authId = :authId", | |||||
| // authBatchDeleteValues); | |||||
| // } | |||||
| return instance; | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public User newRecord(NewUserReq req) throws UnsupportedEncodingException { | |||||
| if (findByUsername(req.getUsername()).isPresent()) { | |||||
| throw new UnprocessableEntityException(ErrorCodes.USERNAME_NOT_AVAILABLE); | |||||
| } | |||||
| String randomPassword = PasswordUtils.genPwd(new PasswordRule(settingsService)); | |||||
| String pwdHash = passwordEncoder.encode(randomPassword); | |||||
| User instance = new User(); | |||||
| instance.setPassword(pwdHash); | |||||
| instance = saveOrUpdate(instance, req); | |||||
| // Locale locale = instance.getLocale() != null ? LocaleUtils.from(instance.getLocale()) : Locale.ENGLISH; | |||||
| // mailService.send( | |||||
| // MailRequest.builder() | |||||
| // .subject(messageSource.getMessage("USER.newAc.subject", null, locale)) | |||||
| // .template("mail/newUser") | |||||
| // .args(Map.of("username", instance.getUsername(), "password", StringEscapeUtils.escapeHtml4(randomPassword))) | |||||
| // .addTo(new InternetAddress(instance.getEmail(), instance.getName())) | |||||
| // .build(), | |||||
| // locale); | |||||
| return instance; | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public User newPublicUserRecord(NewPublicUserReq req) throws UnsupportedEncodingException { | |||||
| if (findByUsername(req.getUsername()).isPresent()) { | |||||
| throw new UnprocessableEntityException(ErrorCodes.USERNAME_NOT_AVAILABLE); | |||||
| } | |||||
| String submitedPassword = req.getPassword(); | |||||
| String pwdHash = passwordEncoder.encode(submitedPassword); | |||||
| req.setPassword(pwdHash); | |||||
| User instance = new User(); | |||||
| instance = saveOrUpdate(instance, req); | |||||
| return instance; | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public void updateRecord(long id, UpdateUserReq req) { | |||||
| saveOrUpdate( | |||||
| find(id).orElseThrow(NotFoundException::new), | |||||
| req); | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public String resetPassword(long id) throws UnsupportedEncodingException { | |||||
| User instance = find(id).orElseThrow(NotFoundException::new); | |||||
| String randomPassword = PasswordUtils.genPwd(new PasswordRule(settingsService)); | |||||
| instance.setPassword(passwordEncoder.encode(randomPassword)); | |||||
| instance = save(instance); | |||||
| return randomPassword; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,41 @@ | |||||
| package com.ffii.tsms.modules.user.service.pojo; | |||||
| public class AuthRecord { | |||||
| private Integer id; | |||||
| private String module; | |||||
| private String authority; | |||||
| private String name; | |||||
| public Integer getId() { | |||||
| return id; | |||||
| } | |||||
| public void setId(Integer id) { | |||||
| this.id = id; | |||||
| } | |||||
| public String getModule() { | |||||
| return module; | |||||
| } | |||||
| public void setModule(String module) { | |||||
| this.module = module; | |||||
| } | |||||
| public String getAuthority() { | |||||
| return authority; | |||||
| } | |||||
| public void setAuthority(String authority) { | |||||
| this.authority = authority; | |||||
| } | |||||
| public String getName() { | |||||
| return name; | |||||
| } | |||||
| public void setName(String name) { | |||||
| this.name = name; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,155 @@ | |||||
| package com.ffii.tsms.modules.user.service.pojo; | |||||
| import java.time.LocalDateTime; | |||||
| public class UserRecord { | |||||
| private Integer id; | |||||
| private LocalDateTime created; | |||||
| private String createdBy; | |||||
| private String modified; | |||||
| private String modifiedBy; | |||||
| private String username; | |||||
| private Boolean locked; | |||||
| private String name; | |||||
| private Integer companyId; | |||||
| private Integer customerId; | |||||
| private String locale; | |||||
| private String fullname; | |||||
| private String firstname; | |||||
| private String lastname; | |||||
| private String title; | |||||
| private String department; | |||||
| private String deptId; | |||||
| private String email; | |||||
| private String phone1; | |||||
| private String phone2; | |||||
| private String remarks; | |||||
| public Integer getId() { | |||||
| return id; | |||||
| } | |||||
| public void setId(Integer id) { | |||||
| this.id = id; | |||||
| } | |||||
| public LocalDateTime getCreated() { | |||||
| return created; | |||||
| } | |||||
| public void setCreated(LocalDateTime created) { | |||||
| this.created = created; | |||||
| } | |||||
| public String getCreatedBy() { | |||||
| return createdBy; | |||||
| } | |||||
| public void setCreatedBy(String createdBy) { | |||||
| this.createdBy = createdBy; | |||||
| } | |||||
| public String getModified() { | |||||
| return modified; | |||||
| } | |||||
| public void setModified(String modified) { | |||||
| this.modified = modified; | |||||
| } | |||||
| public String getModifiedBy() { | |||||
| return modifiedBy; | |||||
| } | |||||
| public void setModifiedBy(String modifiedBy) { | |||||
| this.modifiedBy = modifiedBy; | |||||
| } | |||||
| public String getUsername() { | |||||
| return username; | |||||
| } | |||||
| public void setUsername(String username) { | |||||
| this.username = username; | |||||
| } | |||||
| public Boolean getLocked() { | |||||
| return locked; | |||||
| } | |||||
| public void setLocked(Boolean locked) { | |||||
| this.locked = locked; | |||||
| } | |||||
| public String getName() { | |||||
| return name; | |||||
| } | |||||
| public void setName(String name) { | |||||
| this.name = name; | |||||
| } | |||||
| public Integer getCompanyId() { | |||||
| return companyId; | |||||
| } | |||||
| public void setCompanyId(Integer companyId) { | |||||
| this.companyId = companyId; | |||||
| } | |||||
| public Integer getCustomerId() { | |||||
| return customerId; | |||||
| } | |||||
| public void setCustomerId(Integer customerId) { | |||||
| this.customerId = customerId; | |||||
| } | |||||
| public String getLocale() { | |||||
| return locale; | |||||
| } | |||||
| public void setLocale(String locale) { | |||||
| this.locale = locale; | |||||
| } | |||||
| public String getFullname() { | |||||
| return fullname; | |||||
| } | |||||
| public void setFullname(String fullname) { | |||||
| this.fullname = fullname; | |||||
| } | |||||
| public String getFirstname() { | |||||
| return firstname; | |||||
| } | |||||
| public void setFirstname(String firstname) { | |||||
| this.firstname = firstname; | |||||
| } | |||||
| public String getLastname() { | |||||
| return lastname; | |||||
| } | |||||
| public void setLastname(String lastname) { | |||||
| this.lastname = lastname; | |||||
| } | |||||
| public String getTitle() { | |||||
| return title; | |||||
| } | |||||
| public void setTitle(String title) { | |||||
| this.title = title; | |||||
| } | |||||
| public String getDepartment() { | |||||
| return department; | |||||
| } | |||||
| public void setDepartment(String department) { | |||||
| this.department = department; | |||||
| } | |||||
| public String getDeptId() { | |||||
| return deptId; | |||||
| } | |||||
| public void setDeptId(String deptId) { | |||||
| this.deptId = deptId; | |||||
| } | |||||
| public String getEmail() { | |||||
| return email; | |||||
| } | |||||
| public void setEmail(String email) { | |||||
| this.email = email; | |||||
| } | |||||
| public String getPhone1() { | |||||
| return phone1; | |||||
| } | |||||
| public void setPhone1(String phone1) { | |||||
| this.phone1 = phone1; | |||||
| } | |||||
| public String getPhone2() { | |||||
| return phone2; | |||||
| } | |||||
| public void setPhone2(String phone2) { | |||||
| this.phone2 = phone2; | |||||
| } | |||||
| public String getRemarks() { | |||||
| return remarks; | |||||
| } | |||||
| public void setRemarks(String remarks) { | |||||
| this.remarks = remarks; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,45 @@ | |||||
| package com.ffii.tsms.modules.user.service.res; | |||||
| import java.util.List; | |||||
| import com.ffii.tsms.modules.user.entity.User; | |||||
| public class LoadUserRes { | |||||
| private User data; | |||||
| private List<Integer> authIds; | |||||
| private List<Integer> groupIds; | |||||
| public LoadUserRes() { | |||||
| } | |||||
| public LoadUserRes(User data, List<Integer> authIds, List<Integer> groupIds) { | |||||
| this.data = data; | |||||
| this.authIds = authIds; | |||||
| this.groupIds = groupIds; | |||||
| } | |||||
| public User getData() { | |||||
| return data; | |||||
| } | |||||
| public void setData(User data) { | |||||
| this.data = data; | |||||
| } | |||||
| public List<Integer> getAuthIds() { | |||||
| return authIds; | |||||
| } | |||||
| public void setAuthIds(List<Integer> authIds) { | |||||
| this.authIds = authIds; | |||||
| } | |||||
| public List<Integer> getGroupIds() { | |||||
| return groupIds; | |||||
| } | |||||
| public void setGroupIds(List<Integer> groupIds) { | |||||
| this.groupIds = groupIds; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,80 @@ | |||||
| package com.ffii.tsms.modules.user.web; | |||||
| import java.util.Map; | |||||
| import org.apache.commons.logging.Log; | |||||
| import org.apache.commons.logging.LogFactory; | |||||
| import org.springframework.http.HttpStatus; | |||||
| import org.springframework.web.bind.ServletRequestBindingException; | |||||
| import org.springframework.web.bind.annotation.DeleteMapping; | |||||
| 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.ResponseStatus; | |||||
| import org.springframework.web.bind.annotation.RestController; | |||||
| import com.ffii.core.exception.NotFoundException; | |||||
| import com.ffii.core.response.IdRes; | |||||
| import com.ffii.core.response.RecordsRes; | |||||
| import com.ffii.core.utils.CriteriaArgsBuilder; | |||||
| import com.ffii.core.utils.Params; | |||||
| import com.ffii.tsms.modules.user.req.SaveGroupReq; | |||||
| import com.ffii.tsms.modules.user.service.GroupService; | |||||
| import jakarta.servlet.http.HttpServletRequest; | |||||
| import jakarta.validation.Valid; | |||||
| @RestController | |||||
| @RequestMapping("/group") | |||||
| public class GroupController{ | |||||
| private final Log logger = LogFactory.getLog(getClass()); | |||||
| private GroupService groupService; | |||||
| public GroupController( | |||||
| GroupService groupService | |||||
| ) { | |||||
| this.groupService = groupService; | |||||
| } | |||||
| @PostMapping("/save") | |||||
| public IdRes saveOrUpdate(@RequestBody @Valid SaveGroupReq req) { | |||||
| return new IdRes(groupService.saveOrUpdate(req).getId()); | |||||
| } | |||||
| @DeleteMapping("/{id}") | |||||
| @ResponseStatus(HttpStatus.NO_CONTENT) | |||||
| public void delete(@PathVariable Long id) { | |||||
| groupService.delete(groupService.find(id).orElseThrow(NotFoundException::new)); | |||||
| } | |||||
| @GetMapping("/{id}") | |||||
| public Map<String, Object> load(@PathVariable Long id) { | |||||
| return Map.of( | |||||
| Params.DATA, groupService.find(id).orElseThrow(NotFoundException::new), | |||||
| "authIds", groupService.listGroupAuthId(id), | |||||
| "userIds", groupService.listGroupUserId(id)); | |||||
| } | |||||
| @GetMapping("/combo") | |||||
| public RecordsRes<Map<String, Object>> comboJson(HttpServletRequest request) throws ServletRequestBindingException { | |||||
| return new RecordsRes<>(groupService.searchForCombo( | |||||
| CriteriaArgsBuilder.withRequest(request) | |||||
| .addInteger(Params.ID) | |||||
| .addStringLike(Params.QUERY) | |||||
| .build())); | |||||
| } | |||||
| @GetMapping | |||||
| public RecordsRes<Map<String, Object>> listJson(HttpServletRequest request) throws ServletRequestBindingException { | |||||
| return new RecordsRes<>(groupService.search( | |||||
| CriteriaArgsBuilder.withRequest(request) | |||||
| .addInteger(Params.ID) | |||||
| .addStringLike(Params.NAME) | |||||
| .addInteger("userId") | |||||
| .build())); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,21 @@ | |||||
| package com.ffii.tsms.modules.user.web; | |||||
| import org.apache.commons.logging.Log; | |||||
| import org.apache.commons.logging.LogFactory; | |||||
| import org.springframework.http.ResponseEntity; | |||||
| import org.springframework.security.access.annotation.Secured; | |||||
| import org.springframework.web.bind.annotation.GetMapping; | |||||
| import org.springframework.web.bind.annotation.RestController; | |||||
| @RestController | |||||
| public class TestController { | |||||
| private final Log logger = LogFactory.getLog(getClass()); | |||||
| @GetMapping("/test") | |||||
| @Secured("ROLE_USER") | |||||
| public ResponseEntity<?> test() throws Exception { | |||||
| logger.info("hihihihihii"); | |||||
| return ResponseEntity.ok("hihi"); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,193 @@ | |||||
| package com.ffii.tsms.modules.user.web; | |||||
| import java.io.UnsupportedEncodingException; | |||||
| import org.apache.commons.logging.Log; | |||||
| import org.apache.commons.logging.LogFactory; | |||||
| import org.springframework.http.HttpStatus; | |||||
| import org.springframework.http.ResponseEntity; | |||||
| import org.springframework.security.access.prepost.PreAuthorize; | |||||
| import org.springframework.security.crypto.password.PasswordEncoder; | |||||
| import org.springframework.web.bind.annotation.DeleteMapping; | |||||
| import org.springframework.web.bind.annotation.GetMapping; | |||||
| import org.springframework.web.bind.annotation.ModelAttribute; | |||||
| import org.springframework.web.bind.annotation.PatchMapping; | |||||
| import org.springframework.web.bind.annotation.PathVariable; | |||||
| import org.springframework.web.bind.annotation.PostMapping; | |||||
| import org.springframework.web.bind.annotation.PutMapping; | |||||
| import org.springframework.web.bind.annotation.RequestBody; | |||||
| import org.springframework.web.bind.annotation.RequestMapping; | |||||
| import org.springframework.web.bind.annotation.ResponseStatus; | |||||
| import org.springframework.web.bind.annotation.RestController; | |||||
| import com.ffii.core.exception.BadRequestException; | |||||
| import com.ffii.core.exception.NotFoundException; | |||||
| import com.ffii.core.exception.UnprocessableEntityException; | |||||
| import com.ffii.core.response.IdRes; | |||||
| import com.ffii.core.utils.PasswordUtils; | |||||
| import com.ffii.tsms.modules.common.ErrorCodes; | |||||
| import com.ffii.tsms.modules.common.PasswordRule; | |||||
| import com.ffii.tsms.modules.common.SecurityUtils; | |||||
| import com.ffii.tsms.modules.settings.service.SettingsService; | |||||
| import com.ffii.tsms.modules.user.entity.User; | |||||
| import com.ffii.tsms.modules.user.req.NewPublicUserReq; | |||||
| import com.ffii.tsms.modules.user.req.NewUserReq; | |||||
| import com.ffii.tsms.modules.user.req.SearchUserReq; | |||||
| import com.ffii.tsms.modules.user.req.UpdateUserReq; | |||||
| import com.ffii.tsms.modules.user.service.UserService; | |||||
| import com.ffii.tsms.modules.user.service.res.LoadUserRes; | |||||
| import jakarta.validation.Valid; | |||||
| import jakarta.validation.constraints.NotBlank; | |||||
| @RestController | |||||
| @RequestMapping("/user") | |||||
| public class UserController{ | |||||
| private final Log logger = LogFactory.getLog(getClass()); | |||||
| private UserService userService; | |||||
| private PasswordEncoder passwordEncoder; | |||||
| private SettingsService settingsService; | |||||
| public UserController( | |||||
| UserService userService, | |||||
| PasswordEncoder passwordEncoder, | |||||
| SettingsService settingsService) { | |||||
| this.userService = userService; | |||||
| this.passwordEncoder = passwordEncoder; | |||||
| this.settingsService = settingsService; | |||||
| } | |||||
| // @Operation(summary = "list user", responses = { @ApiResponse(responseCode = "200"), | |||||
| // @ApiResponse(responseCode = "404", content = @Content) }) | |||||
| @GetMapping | |||||
| @PreAuthorize("hasAuthority('VIEW_USER')") | |||||
| public ResponseEntity<?> list(@ModelAttribute @Valid SearchUserReq req) { | |||||
| logger.info("Test List user"); | |||||
| return ResponseEntity.ok(userService.search(req)); | |||||
| } | |||||
| // @Operation(summary = "load user data", responses = { @ApiResponse(responseCode = "200"), | |||||
| // @ApiResponse(responseCode = "404", content = @Content) }) | |||||
| @GetMapping("/{id}") | |||||
| @PreAuthorize("hasAuthority('VIEW_USER')") | |||||
| public LoadUserRes load(@PathVariable long id) { | |||||
| LoadUserRes test = new LoadUserRes( | |||||
| userService.find(id).orElseThrow(NotFoundException::new), | |||||
| userService.listUserAuthId(id), | |||||
| userService.listUserGroupId(id)); | |||||
| logger.info("Test List user2"); | |||||
| logger.info(test); | |||||
| return test; | |||||
| } | |||||
| // @Operation(summary = "delete user", responses = { @ApiResponse(responseCode = "204"), | |||||
| // @ApiResponse(responseCode = "404", content = @Content) }) | |||||
| @DeleteMapping("/{id}") | |||||
| @ResponseStatus(HttpStatus.NO_CONTENT) | |||||
| @PreAuthorize("hasAuthority('MAINTAIN_USER')") | |||||
| public void delete(@PathVariable long id) { | |||||
| userService.markDelete(userService.find(id).orElseThrow(NotFoundException::new)); | |||||
| } | |||||
| // @Operation(summary = "new user") | |||||
| @PostMapping | |||||
| @ResponseStatus(HttpStatus.CREATED) | |||||
| @PreAuthorize("hasAuthority('MAINTAIN_USER')") | |||||
| public IdRes newRecord(@RequestBody @Valid NewUserReq req) throws UnsupportedEncodingException { | |||||
| return new IdRes(userService.newRecord(req).getId()); | |||||
| } | |||||
| // @Operation(summary = "new user by public user") | |||||
| @PostMapping("/registry") | |||||
| @ResponseStatus(HttpStatus.CREATED) | |||||
| // @PreAuthorize("hasAuthority('MAINTAIN_USER')") | |||||
| public ResponseEntity<?> createPublicUserRecord(@RequestBody NewPublicUserReq req) throws UnsupportedEncodingException { | |||||
| logger.info("Create user request:"); | |||||
| return ResponseEntity.ok(new IdRes(userService.newPublicUserRecord(req).getId())) ; | |||||
| } | |||||
| // @Operation(summary = "update user", responses = { | |||||
| // @ApiResponse(responseCode = "204"), | |||||
| // @ApiResponse(responseCode = "400", content = @Content), | |||||
| // @ApiResponse(responseCode = "404", content = @Content), | |||||
| // }) | |||||
| @PutMapping("/{id}") | |||||
| @ResponseStatus(HttpStatus.NO_CONTENT) | |||||
| @PreAuthorize("hasAuthority('MAINTAIN_USER')") | |||||
| public void updateRecord(@PathVariable int id, @RequestBody @Valid UpdateUserReq req) { | |||||
| userService.updateRecord(id, req); | |||||
| } | |||||
| // @Operation(summary = "current user change password", description = "error: USER_WRONG_NEW_PWD = new password not available", responses = { | |||||
| // @ApiResponse(responseCode = "204"), | |||||
| // @ApiResponse(responseCode = "400", content = @Content), | |||||
| // @ApiResponse(responseCode = "404", content = @Content), | |||||
| // @ApiResponse(responseCode = "422", content = @Content(schema = @Schema(implementation = FailureRes.class))), | |||||
| // }) | |||||
| @PatchMapping("/change-password") | |||||
| @ResponseStatus(HttpStatus.NO_CONTENT) | |||||
| // @PreAuthorize("hasAuthority('MAINTAIN_USER')") | |||||
| public void changePassword(@RequestBody @Valid ChangePwdReq req) { | |||||
| long id = SecurityUtils.getUser().get().getId(); | |||||
| User instance = userService.find(id).orElseThrow(NotFoundException::new); | |||||
| logger.info("TEST req: "+req.getPassword()); | |||||
| logger.info("TEST instance: "+instance.getPassword()); | |||||
| if (!passwordEncoder.matches(req.getPassword(), instance.getPassword())) { | |||||
| throw new BadRequestException(); | |||||
| } | |||||
| PasswordRule rule = new PasswordRule(settingsService); | |||||
| if (!PasswordUtils.checkPwd(req.getNewPassword(), rule)) { | |||||
| throw new UnprocessableEntityException(ErrorCodes.USER_WRONG_NEW_PWD); | |||||
| } | |||||
| instance.setPassword(passwordEncoder.encode(req.getNewPassword())); | |||||
| userService.save(instance); | |||||
| } | |||||
| // @Operation(summary = "reset password", responses = { | |||||
| // @ApiResponse(responseCode = "204"), | |||||
| // @ApiResponse(responseCode = "404", content = @Content), | |||||
| // }) | |||||
| @PostMapping("/{id}/reset-password") | |||||
| @PreAuthorize("hasAuthority('MAINTAIN_USER')") | |||||
| @ResponseStatus(HttpStatus.NO_CONTENT) | |||||
| public ResponseEntity<?> resetPassword(@PathVariable long id) throws UnsupportedEncodingException { | |||||
| String password = userService.resetPassword(id); | |||||
| return ResponseEntity.ok(password); | |||||
| } | |||||
| // @Operation(summary = "get password rules") | |||||
| @GetMapping("/password-rule") | |||||
| public PasswordRule passwordRlue() { | |||||
| return new PasswordRule(settingsService); | |||||
| } | |||||
| public static class ChangePwdReq { | |||||
| @NotBlank | |||||
| private String password; | |||||
| @NotBlank | |||||
| private String newPassword; | |||||
| public String getPassword() { | |||||
| return password; | |||||
| } | |||||
| public void setPassword(String password) { | |||||
| this.password = password; | |||||
| } | |||||
| public String getNewPassword() { | |||||
| return newPassword; | |||||
| } | |||||
| public void setNewPassword(String newPassword) { | |||||
| this.newPassword = newPassword; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,5 @@ | |||||
| spring: | |||||
| datasource: | |||||
| jdbc-url: jdbc:mysql://192.168.1.81:3306/arsdb?useUnicode=true&characterEncoding=UTF8&serverTimezone=GMT%2B8 | |||||
| username: root | |||||
| password: secret | |||||
| @@ -0,0 +1,5 @@ | |||||
| spring: | |||||
| datasource: | |||||
| jdbc-url: jdbc:mysql://127.0.0.1:3306/tsmsdb?useUnicode=true&characterEncoding=UTF8&serverTimezone=GMT%2B8 | |||||
| username: root | |||||
| password: secret | |||||
| @@ -0,0 +1,9 @@ | |||||
| spring: | |||||
| ldap: | |||||
| embedded: | |||||
| port: 8389 | |||||
| base-dn: dc=springframework,dc=org | |||||
| ldif: classpath:ldap-test-users.ldif | |||||
| validation: | |||||
| enabled: false | |||||
| urls: ldap://localhost:8389 | |||||
| @@ -0,0 +1,2 @@ | |||||
| logging: | |||||
| config: 'classpath:log4j2-prod-linux.yml' | |||||
| @@ -0,0 +1,2 @@ | |||||
| logging: | |||||
| config: 'classpath:log4j2-prod-win.yml' | |||||
| @@ -0,0 +1,28 @@ | |||||
| server: | |||||
| servlet: | |||||
| contextPath: /api | |||||
| encoding: | |||||
| charset: UTF-8 | |||||
| enabled: true | |||||
| force: true | |||||
| port: 8090 | |||||
| error: | |||||
| include-message: always | |||||
| spring: | |||||
| servlet: | |||||
| multipart: | |||||
| max-file-size: 500MB | |||||
| max-request-size: 600MB | |||||
| jpa: | |||||
| hibernate: | |||||
| naming: | |||||
| physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl | |||||
| database-platform: org.hibernate.dialect.MySQL8Dialect | |||||
| properties: | |||||
| hibernate: | |||||
| dialect: | |||||
| storage_engine: innodb | |||||
| logging: | |||||
| config: 'classpath:log4j2.yml' | |||||
| @@ -0,0 +1,77 @@ | |||||
| --liquibase formatted sql | |||||
| --changeset alex:user | |||||
| --comment: core table | |||||
| CREATE TABLE `user` ( | |||||
| `id` int NOT NULL AUTO_INCREMENT, | |||||
| `created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, | |||||
| `createdBy` varchar(30) DEFAULT NULL, | |||||
| `version` int NOT NULL DEFAULT '0', | |||||
| `modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, | |||||
| `modifiedBy` varchar(30) DEFAULT NULL, | |||||
| `deleted` tinyint(1) NOT NULL DEFAULT '0', | |||||
| `username` varchar(30) NOT NULL, | |||||
| `password` varchar(60) DEFAULT NULL, | |||||
| `locked` tinyint(1) NOT NULL DEFAULT '0', | |||||
| `expiryDate` date DEFAULT NULL, | |||||
| `name` varchar(50) NOT NULL, | |||||
| `locale` varchar(5) DEFAULT NULL, | |||||
| `fullname` varchar(90) DEFAULT NULL, | |||||
| `firstname` varchar(45) DEFAULT NULL, | |||||
| `lastname` varchar(30) DEFAULT NULL, | |||||
| `title` varchar(60) DEFAULT NULL, | |||||
| `department` varchar(60) DEFAULT NULL, | |||||
| `email` varchar(120) DEFAULT NULL, | |||||
| `phone1` varchar(30) DEFAULT NULL, | |||||
| `phone2` varchar(30) DEFAULT NULL, | |||||
| `remarks` varchar(600) DEFAULT NULL, | |||||
| `lotusNotesUser` tinyint(1) NOT NULL DEFAULT '0', | |||||
| PRIMARY KEY (`id`), | |||||
| UNIQUE KEY `username` (`username`) | |||||
| ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; | |||||
| INSERT INTO `user`(`name`,`username`, `password`)VALUES ('2fi','2fi','$2a$10$65S7/AhKn8MldlYmvFN5JOfr1yaULwFNDIhTskLTuUCKgbbs8sFAi'); | |||||
| INSERT INTO `user`(`name`,`username`, `password`,`lotusNotesUser`)VALUES ('user1','user1',null,1); | |||||
| CREATE TABLE `authority` ( | |||||
| `id` int NOT NULL AUTO_INCREMENT, | |||||
| `authority` varchar(255) NOT NULL, | |||||
| `name` varchar(100) NOT NULL, | |||||
| `module` varchar(50) DEFAULT NULL, | |||||
| `description` varchar(255) DEFAULT NULL, | |||||
| PRIMARY KEY (`id`), | |||||
| UNIQUE KEY `authority` (`authority`) | |||||
| ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; | |||||
| INSERT INTO `authority` VALUES (1,'MAINTAIN_USER','Maintain User',NULL,NULL),(2,'MAINTAIN_GROUP','Maintain group',NULL,NULL),(3,'VIEW_USER','view user',NULL,NULL),(4,'VIEW_GROUP','view group',NULL,NULL); | |||||
| CREATE TABLE `user_authority` ( | |||||
| `userId` int NOT NULL, | |||||
| `authId` int NOT NULL, | |||||
| PRIMARY KEY (`userId`,`authId`) | |||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; | |||||
| INSERT INTO `user_authority` VALUES (1,3); | |||||
| --changeset alex:group | |||||
| --comment: group table | |||||
| CREATE TABLE `group` ( | |||||
| `id` int NOT NULL AUTO_INCREMENT, | |||||
| `created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, | |||||
| `createdBy` varchar(30) DEFAULT NULL, | |||||
| `version` int NOT NULL DEFAULT '0', | |||||
| `modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, | |||||
| `modifiedBy` varchar(30) DEFAULT NULL, | |||||
| `deleted` tinyint(1) NOT NULL DEFAULT '0', | |||||
| `name` varchar(50) NOT NULL, | |||||
| `description` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_bin DEFAULT NULL, | |||||
| PRIMARY KEY (`id`), | |||||
| UNIQUE KEY `name` (`name`) | |||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; | |||||
| CREATE TABLE `user_group` ( | |||||
| `groupId` int NOT NULL, | |||||
| `userId` int NOT NULL, | |||||
| PRIMARY KEY (`groupId`,`userId`) | |||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; | |||||
| CREATE TABLE `group_authority` ( | |||||
| `groupId` int NOT NULL, | |||||
| `authId` int NOT NULL, | |||||
| PRIMARY KEY (`groupId`,`authId`) | |||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; | |||||
| @@ -0,0 +1,13 @@ | |||||
| --liquibase formatted sql | |||||
| --changeset alex:settings | |||||
| --comment: settings table | |||||
| CREATE TABLE `settings` ( | |||||
| `id` INT PRIMARY KEY AUTO_INCREMENT, | |||||
| `name` varchar(255) NOT NULL, | |||||
| `value` varchar(1000) NOT NULL, | |||||
| `category` varchar(50), | |||||
| `type` varchar(10), | |||||
| INDEX `name_idx` (`name`) | |||||
| ); | |||||
| @@ -0,0 +1,10 @@ | |||||
| --liquibase formatted sql | |||||
| --changeset alex:settings_data | |||||
| INSERT INTO `settings` (`name`, `value`,`type`) VALUES | |||||
| ('SYS.password.rule.length.max', '20', 'integer'), | |||||
| ('SYS.password.rule.length.min', '8', 'integer'), | |||||
| ('SYS.password.rule.lower.eng', 'true', 'boolean'), | |||||
| ('SYS.password.rule.number', 'true', 'boolean'), | |||||
| ('SYS.password.rule.special', 'true', 'boolean'), | |||||
| ('SYS.password.rule.upper.eng', 'true', 'boolean'); | |||||
| @@ -0,0 +1,5 @@ | |||||
| --liquibase formatted sql | |||||
| --changeset alex:update_user_authority | |||||
| INSERT INTO `tsmsdb`.`user_authority` (`userId`, `authId`) VALUES ('1', '1'); | |||||
| INSERT INTO `tsmsdb`.`user_authority` (`userId`, `authId`) VALUES ('1', '2'); | |||||
| @@ -0,0 +1,13 @@ | |||||
| --liquibase formatted sql | |||||
| --changeset alex:audit_log | |||||
| --comment: audit log | |||||
| CREATE TABLE `audit_log` ( | |||||
| `tableName` varchar(30) NOT NULL, | |||||
| `recordId` int(11) NOT NULL, | |||||
| `modifiedBy` int(11) DEFAULT NULL, | |||||
| `modified` datetime DEFAULT NULL, | |||||
| `oldData` json DEFAULT NULL, | |||||
| `newData` json DEFAULT NULL, | |||||
| KEY `idx_tableName_recordId` (`tableName`,`recordId`) | |||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8; | |||||
| @@ -0,0 +1,11 @@ | |||||
| --liquibase formatted sql | |||||
| --changeset alex:user_login_log | |||||
| --comment: user login log | |||||
| CREATE TABLE `user_login_log` ( | |||||
| `username` varchar(32) NOT NULL, | |||||
| `loginTime` datetime NOT NULL, | |||||
| `ipAddr` varchar(45) NOT NULL, | |||||
| `success` tinyint(1) NOT NULL, | |||||
| PRIMARY KEY (`username`,`loginTime`) | |||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8; | |||||
| @@ -0,0 +1,3 @@ | |||||
| databaseChangeLog: | |||||
| - includeAll: | |||||
| path: classpath:/db/changelog/changes | |||||
| @@ -0,0 +1,14 @@ | |||||
| dn: dc=springframework,dc=org | |||||
| objectClass: top | |||||
| objectClass: domain | |||||
| dc: springframework | |||||
| dn: uid=user1,dc=springframework,dc=org | |||||
| objectClass: top | |||||
| cn: user1 | |||||
| userPassword: userPass1 | |||||
| dn: uid=user2,dc=springframework,dc=org | |||||
| objectClass: top | |||||
| cn: user2 | |||||
| userPassword: userPass2 | |||||
| @@ -0,0 +1,23 @@ | |||||
| Configutation: | |||||
| name: Prod-Default | |||||
| Properties: | |||||
| Property: | |||||
| name: log_location | |||||
| value: /usr/springboot/logs/ | |||||
| Appenders: | |||||
| RollingFile: | |||||
| name: RollingFile_Appender | |||||
| fileName: ${log_location}tsms-all.log | |||||
| filePattern: ${log_location}tsms-all.log.%i.gz | |||||
| PatternLayout: | |||||
| Pattern: "%d %p [%l] - %m%n" | |||||
| Policies: | |||||
| SizeBasedTriggeringPolicy: | |||||
| size: 4096KB | |||||
| DefaultRollOverStrategy: | |||||
| max: 99 | |||||
| Loggers: | |||||
| Root: | |||||
| level: info | |||||
| AppenderRef: | |||||
| - ref: RollingFile_Appender | |||||
| @@ -0,0 +1,23 @@ | |||||
| Configutation: | |||||
| name: Prod-Default | |||||
| Properties: | |||||
| Property: | |||||
| name: log_location | |||||
| value: C:/workspace/ | |||||
| Appenders: | |||||
| RollingFile: | |||||
| name: RollingFile_Appender | |||||
| fileName: ${log_location}tsms-all.log | |||||
| filePattern: ${log_location}tsms-all.log.%i.gz | |||||
| PatternLayout: | |||||
| Pattern: "%d %p [%l] - %m%n" | |||||
| Policies: | |||||
| SizeBasedTriggeringPolicy: | |||||
| size: 4096KB | |||||
| DefaultRollOverStrategy: | |||||
| max: 99 | |||||
| Loggers: | |||||
| Root: | |||||
| level: info | |||||
| AppenderRef: | |||||
| - ref: RollingFile_Appender | |||||
| @@ -0,0 +1,17 @@ | |||||
| Configutation: | |||||
| name: Default | |||||
| Properties: | |||||
| Property: | |||||
| name: log_pattern | |||||
| value: "%d{yyyy-MM-dd HH:mm:ss.SSS} %5p ${hostName} --- [%15.15t] %-40.40c{1.} : %m%n%ex" | |||||
| Appenders: | |||||
| Console: | |||||
| name: Console_Appender | |||||
| target: SYSTEM_OUT | |||||
| PatternLayout: | |||||
| pattern: ${log_pattern} | |||||
| Loggers: | |||||
| Root: | |||||
| level: info | |||||
| AppenderRef: | |||||
| - ref: Console_Appender | |||||
| @@ -0,0 +1,13 @@ | |||||
| package com.ffii.tsms; | |||||
| import org.junit.jupiter.api.Test; | |||||
| import org.springframework.boot.test.context.SpringBootTest; | |||||
| @SpringBootTest | |||||
| class TsmsApplicationTests { | |||||
| @Test | |||||
| void contextLoads() { | |||||
| } | |||||
| } | |||||