Index: .gitignore =================================================================== diff -u --- .gitignore (revision 0) +++ .gitignore (revision 3ecf807f4027e53b3aaac2ffc17fc3617bfcc641) @@ -0,0 +1,17 @@ +Thumbs.db +.DS_Store +.gradle +build/ + +### Intellij +.idea +*.iml +*.ipr +*.iws +out + +### Eclipse +.project +.settings +.classpath +bin Index: HELP.md =================================================================== diff -u --- HELP.md (revision 0) +++ HELP.md (revision 3ecf807f4027e53b3aaac2ffc17fc3617bfcc641) @@ -0,0 +1,14 @@ +# Getting Started + +### Reference Documentation +For further reference, please consider the following sections: + +* [Official Gradle documentation](https://docs.gradle.org) +* [Spring Boot Gradle Plugin Reference Guide](https://docs.spring.io/spring-boot/docs/2.3.2.RELEASE/gradle-plugin/reference/html/) +* [Create an OCI image](https://docs.spring.io/spring-boot/docs/2.3.2.RELEASE/gradle-plugin/reference/html/#build-image) + +### Additional Links +These additional references should also help you: + +* [Gradle Build Scans – insights for your project's build](https://scans.gradle.com#gradle) + Index: build.gradle =================================================================== diff -u --- build.gradle (revision 0) +++ build.gradle (revision 3ecf807f4027e53b3aaac2ffc17fc3617bfcc641) @@ -0,0 +1,106 @@ +buildscript { + repositories { + mavenLocal() + maven { url 'https://workhorse.lemanscorp.com/nexus/content/groups/public/' } + } + dependencies { + classpath 'com.lemans.boot.common:lemans-gradle:0.0.1k' + } +} + +plugins { + id 'org.springframework.boot' version '2.2.5.RELEASE' + id 'io.spring.dependency-management' version '1.0.9.RELEASE' + id 'war' + id 'groovy' + id 'codenarc' +} + +apply plugin: 'lemans-war' + +group = 'com.lemans' +sourceCompatibility = '1.8' + +defaultTasks 'clean', 'check' + +repositories { + mavenLocal() + maven { url 'https://workhorse.lemanscorp.com/nexus/content/groups/public/' } +} + +configurations { + compile.exclude module: 'commons-logging' +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-jdbc' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.codehaus.groovy:groovy' + runtimeOnly 'com.microsoft.sqlserver:mssql-jdbc' + compileOnly 'org.springframework.boot:spring-boot-starter-tomcat' + testImplementation('org.springframework.boot:spring-boot-starter-test') { + exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' + } + + + implementation 'org.codehaus.groovy:groovy-all:3.0.0' + + implementation 'log4j:log4j:1.2.17' + implementation "org.slf4j:slf4j-api:1.7.25" + implementation "org.slf4j:jcl-over-slf4j:1.7.25" + implementation "org.slf4j:jul-to-slf4j:1.7.25" + implementation "org.slf4j:log4j-over-slf4j:1.7.25" + implementation "ch.qos.logback:logback-core" + implementation "ch.qos.logback:logback-classic" + + implementation 'com.fasterxml.jackson.core:jackson-databind' + + implementation 'org.apache.httpcomponents:httpmime:4.5.5' + implementation 'org.apache.httpcomponents:httpclient:4.5.5' + implementation 'org.codehaus.groovy.modules.http-builder:http-builder:0.7.1' + + implementation 'org.spockframework:spock-core:2.0-M2-groovy-2.5' + implementation 'org.spockframework:spock-spring:2.0-M2-groovy-2.5' + + + implementation 'com.lemans.boot.common:lemans-testing:0.0.1r' + implementation 'com.lemans.boot.common:lemans-security:0.0.1r' + implementation 'com.lemans.boot.common:lemans-rest:0.0.1r' + implementation 'com.lemans.boot.common:lemans-core:0.0.1r' + implementation 'com.lemans.boot.common:lemans-gradle:0.0.1r' + + implementation group: 'commons-dbcp', name: 'commons-dbcp', version: '1.4' + + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + compile group: 'com.opencsv', name: 'opencsv', version: '3.3' + compile group: 'org.json', name: 'json', version: '20090211' + + compile group: 'commons-validator', name: 'commons-validator', version: '1.4.0' + compile group: 'javax.mail', name: 'mail', version: '1.4.1' + + compile 'dumbster:dumbster:1.6' + compile 'javax.mail:mail:1.4.5' + + compile group: 'org.springframework.boot', name: 'spring-boot-starter-mail', version: '1.2.0.RELEASE' + +} + +test { + useJUnitPlatform() +} + +tasks.withType(Test) { + testLogging { + showStandardStreams = true + } + systemProperties System.properties +} + +tasks.withType(JavaExec) { + systemProperties System.properties +} + +codenarc { + toolVersion = '1.0' + configFile = file("${project.projectDir}/codenarc/rules.groovy") +} Index: codenarc/rules.groovy =================================================================== diff -u --- codenarc/rules.groovy (revision 0) +++ codenarc/rules.groovy (revision 3ecf807f4027e53b3aaac2ffc17fc3617bfcc641) @@ -0,0 +1,78 @@ +ruleset { + ruleset('rulesets/basic.xml') + ruleset('rulesets/braces.xml') + ruleset('rulesets/concurrency.xml') + ruleset('rulesets/convention.xml') { + exclude 'NoDef' + exclude 'NoTabCharacter' + exclude 'TrailingComma' + } + ruleset('rulesets/design.xml') { + exclude 'AbstractClassWithoutAbstractMethod' + exclude 'EmptyMethodInAbstractClass' + exclude 'SimpleDateFormatMissingLocale' + } + ruleset('rulesets/dry.xml') { + exclude 'DuplicateListLiteral' + exclude 'DuplicateNumberLiteral' + exclude 'DuplicateMapLiteral' + exclude 'DuplicateStringLiteral' + } + ruleset('rulesets/enhanced.xml') + ruleset('rulesets/exceptions.xml') + ruleset('rulesets/formatting.xml') { + exclude 'ClassJavadoc' + exclude 'ConsecutiveBlankLines' + LineLength { + length = 200 + } + SpaceAroundMapEntryColon { + characterAfterColonRegex = /\s/ + } + exclude 'TrailingWhitespace' + } + ruleset('rulesets/generic.xml') + ruleset('rulesets/grails.xml') { + exclude 'GrailsDomainHasEquals' + exclude 'GrailsDomainHasToString' + } + ruleset('rulesets/groovyism.xml') + ruleset('rulesets/imports.xml') + ruleset('rulesets/jdbc.xml') + ruleset('rulesets/junit.xml') + ruleset('rulesets/logging.xml') + ruleset('rulesets/naming.xml') { + exclude 'FactoryMethodName' + MethodName { // helps Spock + regex = /[a-zA-Z#][_#.\w\s'"\(\)]*/ + } + PropertyName { + regex = /[a-z][_a-zA-Z0-9]*/ + } + } + ruleset('rulesets/security.xml') { + exclude 'JavaIoPackageAccess' + } + ruleset('rulesets/serialization.xml') + ruleset('rulesets/size.xml') { + CyclomaticComplexity { // Requires the GMetrics jar + maxMethodComplexity = 15 + } + MethodSize { + maxLines = 26 + doNotApplyToClassNames = '*Spec' + } + MethodSize { + maxLines = 41 + applyToClassNames = '*Spec' + } + } + ruleset('rulesets/unnecessary.xml') { + UnnecessaryBooleanExpression { + doNotApplyToClassNames = '*Spec*' + } + exclude 'UnnecessaryGString' + + } + ruleset('rulesets/unused.xml') +} \ No newline at end of file Index: gradle/wrapper/gradle-wrapper.jar =================================================================== diff -u Binary files differ Index: gradle/wrapper/gradle-wrapper.properties =================================================================== diff -u --- gradle/wrapper/gradle-wrapper.properties (revision 0) +++ gradle/wrapper/gradle-wrapper.properties (revision 3ecf807f4027e53b3aaac2ffc17fc3617bfcc641) @@ -0,0 +1,5 @@ +distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-all.zip +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME Index: gradlew =================================================================== diff -u --- gradlew (revision 0) +++ gradlew (revision 3ecf807f4027e53b3aaac2ffc17fc3617bfcc641) @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or 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 UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$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 "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# 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 + ;; + 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" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" Index: gradlew.bat =================================================================== diff -u --- gradlew.bat (revision 0) +++ gradlew.bat (revision 3ecf807f4027e53b3aaac2ffc17fc3617bfcc641) @@ -0,0 +1,104 @@ +@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%" == "0" goto init + +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 init + +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 + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +: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 %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="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! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega Index: settings.gradle =================================================================== diff -u --- settings.gradle (revision 0) +++ settings.gradle (revision 3ecf807f4027e53b3aaac2ffc17fc3617bfcc641) @@ -0,0 +1,7 @@ +pluginManagement { + repositories { + maven { url 'https://workhorse.lemanscorp.com/nexus/content/groups/public/' } + } +} + +rootProject.name = 'correspondence-service' Index: src/main/groovy/com/lemans/correspondence/BootStrap.groovy =================================================================== diff -u --- src/main/groovy/com/lemans/correspondence/BootStrap.groovy (revision 0) +++ src/main/groovy/com/lemans/correspondence/BootStrap.groovy (revision 3ecf807f4027e53b3aaac2ffc17fc3617bfcc641) @@ -0,0 +1,28 @@ +package com.lemans.correspondence + +import com.dumbster.smtp.SmtpMessage +import org.springframework.stereotype.Component + +import javax.annotation.PostConstruct + +@Component +class BootStrap { + + @PostConstruct + void init() { + MetaClass mc = SmtpMessage.metaClass + Closure split = { it.split(',')*.trim() } + mc.with { + getSubject = { -> delegate.getHeaderValue('Subject') } + getDate = { -> delegate.getHeaderValue('Date') } + getTo = { -> delegate.tos[0] } + getTos = { -> split(delegate.getHeaderValue('To')) } + getFrom = { -> delegate.froms[0] } + getFroms = { -> split(delegate.getHeaderValue('From')) } + getCc = { -> delegate.ccs[0] } + getCcs = { -> split(delegate.getHeaderValue('Cc')) } + getBcc = { -> delegate.bccs[0] } + getBccs = { -> split(delegate.getHeaderValue('Bcc')) } + } + } +} Index: src/main/groovy/com/lemans/correspondence/CorrespondenceApplication.groovy =================================================================== diff -u --- src/main/groovy/com/lemans/correspondence/CorrespondenceApplication.groovy (revision 0) +++ src/main/groovy/com/lemans/correspondence/CorrespondenceApplication.groovy (revision 3ecf807f4027e53b3aaac2ffc17fc3617bfcc641) @@ -0,0 +1,14 @@ +package com.lemans.correspondence + +import com.lemans.services.LemansApplication +import org.springframework.boot.SpringApplication +import org.springframework.boot.autoconfigure.SpringBootApplication + + +@SpringBootApplication +class CorrespondenceApplication extends LemansApplication { + + static void main(String[] args) { + SpringApplication.run(CorrespondenceApplication, args) + } +} Index: src/main/groovy/com/lemans/correspondence/ServletInitializer.groovy =================================================================== diff -u --- src/main/groovy/com/lemans/correspondence/ServletInitializer.groovy (revision 0) +++ src/main/groovy/com/lemans/correspondence/ServletInitializer.groovy (revision 3ecf807f4027e53b3aaac2ffc17fc3617bfcc641) @@ -0,0 +1,13 @@ +package com.lemans.correspondence + +import org.springframework.boot.builder.SpringApplicationBuilder +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer + +class ServletInitializer extends SpringBootServletInitializer { + + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { + application.sources(CorrespondenceApplication) + } + +} Index: src/main/groovy/com/lemans/correspondence/controllers/DomainFormController.groovy =================================================================== diff -u --- src/main/groovy/com/lemans/correspondence/controllers/DomainFormController.groovy (revision 0) +++ src/main/groovy/com/lemans/correspondence/controllers/DomainFormController.groovy (revision 3ecf807f4027e53b3aaac2ffc17fc3617bfcc641) @@ -0,0 +1,76 @@ +package com.lemans.correspondence.controllers + +import com.lemans.api.LeMansApiController +import com.lemans.correspondence.domain.forms.DomainForm +import com.lemans.correspondence.services.DomainFormManagerService +import com.lemans.correspondence.services.DomainFormService +import org.springframework.beans.factory.annotation.Autowired +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.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +import javax.validation.Valid + +@RestController +@RequestMapping("dm/{dm}") +class DomainFormController extends LeMansApiController { + + @Autowired + DomainFormService domainFormService + + @Autowired + DomainFormManagerService domainFormManagerService + + @GetMapping(value = "/domain/{domainId}/form") + def index(@PathVariable Integer domainId) { + Map criteria = common() + pagination() + [domainId: domainId] + Map data = domainFormService.findForms(criteria) + renderMany data.results, data.totalRecords + } + + @GetMapping(value = "/domain/{domainId}/form/{formKey}") + def showByDomainIdAndKey(@PathVariable Integer domainId, @PathVariable String formKey) { + Map criteria = common() + [domainId: domainId, formKey: formKey] + renderOne domainFormService.findForm(criteria) + } + + def show(Integer domainFormId, Integer domainId) { + renderOne domainFormService.findForm(common() + [id: domainFormId, domainId: domainId]) + } + + @GetMapping(value = "/form/type") + def types() { + renderMany domainFormService.allFormTypes() + } + + @GetMapping(value = "/form/type/{id}") + def typeById(@PathVariable Integer id) { + renderOne domainFormService.formTypeById(id) + } + + @PostMapping(value = "/domain/{domainId}/form") + def add(@PathVariable Integer domainId, @Valid @RequestBody DomainForm domainForm) { + DomainForm form = domainFormManagerService.createDomainForm(domainForm, auditUserName, domainId) + form?.hasErrors() ? renderErrors(form) : show(form.id, form.domain.id) + } + + @PutMapping(value = "/domain/{domainId}/form/{formKey}") + def update(@PathVariable Integer domainId, @PathVariable String formKey, @RequestBody Map input) { + DomainForm form = domainFormManagerService.updateDomainForm(input, formKey, auditUserName, domainId) + form != null ? form?.hasErrors() ? renderErrors(form) : show(form.id, form.domain.id) : notFound(unknownForm()) + } + + @DeleteMapping(value = "/domain/{domainId}/form/{formKey}") + def remove(@PathVariable Integer domainId, @PathVariable String formKey) { + DomainForm form + if (formKey != null) { form = domainFormManagerService.deleteDomainForm(formKey, auditUserName, domainId) } + form != null ? renderDelete(form) : renderDelete(form, unknownForm()) + } + + private List unknownForm() { [errorMessage(unknownEntity('Domain Form'))] } +} Index: src/main/groovy/com/lemans/correspondence/controllers/DumbsterController.groovy =================================================================== diff -u --- src/main/groovy/com/lemans/correspondence/controllers/DumbsterController.groovy (revision 0) +++ src/main/groovy/com/lemans/correspondence/controllers/DumbsterController.groovy (revision 3ecf807f4027e53b3aaac2ffc17fc3617bfcc641) @@ -0,0 +1,31 @@ +package com.lemans.correspondence.controllers + +import com.lemans.api.LeMansApiController +import com.lemans.correspondence.dumbster.Dumbster +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/admin/dumbster") +class DumbsterController extends LeMansApiController { + + Dumbster dumbster + + @GetMapping("/message") + def index() { + if (dumbster) { + render (dumbster.messages) + } + else { notFound() } + } + + @PutMapping("/message/reset") + def reset() { + if (dumbster) { + dumbster.reset() + } + else { notFound() } + } +} Index: src/main/groovy/com/lemans/correspondence/controllers/FormSubmitController.groovy =================================================================== diff -u --- src/main/groovy/com/lemans/correspondence/controllers/FormSubmitController.groovy (revision 0) +++ src/main/groovy/com/lemans/correspondence/controllers/FormSubmitController.groovy (revision 3ecf807f4027e53b3aaac2ffc17fc3617bfcc641) @@ -0,0 +1,69 @@ +package com.lemans.correspondence.controllers + +import com.lemans.api.LeMansApiController +import com.lemans.correspondence.services.DomainFormService +import com.lemans.correspondence.services.FormSubmitService +import org.springframework.beans.factory.annotation.Autowired +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.RequestHeader +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + + +@RestController +@RequestMapping("dm/{dm}") +class FormSubmitController extends LeMansApiController { + + @Autowired + DomainFormService domainFormService + + @Autowired + FormSubmitService formSubmitService + + @PostMapping("/domain/{domainId}/form/{formKey}/submit") + def add(@PathVariable Integer domainId, @PathVariable String formKey, @RequestHeader('content-Type') String contentType, @RequestBody Map input, @RequestParam(required = false) Map params) { + Map formData = [:] + if (contentType.contains('multipart/form-data') || contentType.contains('application/x-www-form-urlencoded')) { + formData = extractFormData(params) + } else { + formData = extractFormData(input) + } + Map domainForm = domainFormService.findForm([domainId: domainId, formKey: formKey]) + if (domainForm) { + List errors = formSubmitService.saveSubmittedForm(domainForm, formData) + if (errors) { + response.status = 400 + renderMessages errors + } + else { renderEmpty() } + } else { notFound(unknownDomainForm()) } + } + + private Map extractFormData(Map inputData) { + Map sortedData = sortFormData(inputData) + sortedData?.collectEntries { + if (it.key?.startsWith('form_')) { + String key = "${it.key?.split('_').last()}" + [(key): URLDecoder.decode(it.value, 'UTF-8')] + } else { + [:] + } + } + } + + private sortFormData(Map inputData) { + inputData.sort { + List data = it.key.split('_') + if (data.size() > 2) { + if (data[1].isNumber()) { + return data[1].toInteger() + } + } + } + } + + private List unknownDomainForm() { [errorMessage(unknownEntity('domainForm'))] } +} Index: src/main/groovy/com/lemans/correspondence/domain/domain/Domain.groovy =================================================================== diff -u --- src/main/groovy/com/lemans/correspondence/domain/domain/Domain.groovy (revision 0) +++ src/main/groovy/com/lemans/correspondence/domain/domain/Domain.groovy (revision 3ecf807f4027e53b3aaac2ffc17fc3617bfcc641) @@ -0,0 +1,31 @@ +package com.lemans.correspondence.domain.domain + +import com.lemans.services.LemansCrudRepository +import groovy.transform.ToString + +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.GeneratedValue +import javax.persistence.GenerationType +import javax.persistence.Id + +@Entity(name = 'domain') +@ToString +class Domain { + + @Id + @GeneratedValue(strategy= GenerationType.IDENTITY) + @Column(name = 'domainId') + Integer id + + @Column(updatable = false) + String domainName + + @Column(updatable = false) + String displayName +} + +interface DomainRepository extends LemansCrudRepository { + + Domain findById(int id) +} Index: src/main/groovy/com/lemans/correspondence/domain/forms/DomainForm.groovy =================================================================== diff -u --- src/main/groovy/com/lemans/correspondence/domain/forms/DomainForm.groovy (revision 0) +++ src/main/groovy/com/lemans/correspondence/domain/forms/DomainForm.groovy (revision 3ecf807f4027e53b3aaac2ffc17fc3617bfcc641) @@ -0,0 +1,56 @@ +package com.lemans.correspondence.domain.forms + +import com.lemans.correspondence.domain.domain.Domain +import com.lemans.services.Auditable +import com.lemans.services.LemansCrudRepository +import groovy.transform.ToString + +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.GeneratedValue +import javax.persistence.GenerationType +import javax.persistence.Id +import javax.persistence.JoinColumn +import javax.persistence.Lob +import javax.persistence.OneToOne +import javax.persistence.Table +import javax.persistence.Transient +import javax.validation.constraints.Size + +@Entity(name = 'DomainForm') +@ToString +@Table +class DomainForm extends Auditable { + + @Id + @GeneratedValue(strategy= GenerationType.IDENTITY) + @Column(name = 'domainFormId') + Integer id + + @OneToOne + @JoinColumn(name = 'domainId') + Domain domain + + Integer domainFormTypeId + + @Column(updatable = false) + String formKey + + @Size(max = 50, message = 'formName ${validatedValue} exceeds the maximum size of {max}') + String formName + + Date startDate + + Date endDate + + @Lob + String formDetailXml + + @Transient + Map form +} + +interface DomainFormRepository extends LemansCrudRepository { + + DomainForm findByFormKeyAndDomain(String formKey, Domain domain) +} Index: src/main/groovy/com/lemans/correspondence/domain/forms/DomainFormType.groovy =================================================================== diff -u --- src/main/groovy/com/lemans/correspondence/domain/forms/DomainFormType.groovy (revision 0) +++ src/main/groovy/com/lemans/correspondence/domain/forms/DomainFormType.groovy (revision 3ecf807f4027e53b3aaac2ffc17fc3617bfcc641) @@ -0,0 +1,22 @@ +package com.lemans.correspondence.domain.forms + +import groovy.transform.ToString + +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.GeneratedValue +import javax.persistence.GenerationType +import javax.persistence.Id + +@Entity(name = 'domainFormType') +@ToString +class DomainFormType { + + @Id + @GeneratedValue(strategy= GenerationType.IDENTITY) + @Column(name = 'domainFormTypeId') + Integer id + + @Column(updatable = false) + String typeName +} Index: src/main/groovy/com/lemans/correspondence/dumbster/Dumbster.groovy =================================================================== diff -u --- src/main/groovy/com/lemans/correspondence/dumbster/Dumbster.groovy (revision 0) +++ src/main/groovy/com/lemans/correspondence/dumbster/Dumbster.groovy (revision 3ecf807f4027e53b3aaac2ffc17fc3617bfcc641) @@ -0,0 +1,90 @@ +package com.lemans.correspondence.dumbster + +import com.dumbster.smtp.SimpleSmtpServer +import com.dumbster.smtp.SmtpMessage +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.mail.javamail.JavaMailSenderImpl +import org.springframework.stereotype.Component + +/** + * @author Burt Beckwith + */ + +@Component +class Dumbster { + + @Autowired + ApplicationContext applicationContext + + Integer port + + protected SimpleSmtpServer server + + /** + * Starts the server; called by Spring, so shouldn't be called directly. + */ + @SuppressWarnings('Println') + void start() { + port = 1025 + while (true) { + try { + new ServerSocket(port).close() + + // update the mail plugin's JavaMailSender if available + if (applicationContext.containsBean('mailSender')) { + JavaMailSenderImpl mailSender = applicationContext.getBean('mailSender') + mailSender.port = port + } + break + } + catch (IOException e) { + port++ + if (port > 10000) { + throw new BindException('Cannot find open port for Dumbster SMTP server') + } + } + } + println "Dumbster is using port $port" + server = SimpleSmtpServer.start(port) + } + + /** + * Stops the server; called by Spring, so shouldn't be called directly. + */ + void stop() { + server?.stop() + } + + /** + * Remove all sent emails. Call this in the setUp() method in your integration tests. + */ + void reset() { + if (!server) { + return + } + + for (Iterator iter = server.receivedEmail; iter.hasNext();) { + iter.next() + iter.remove() + } + } + + /** + * Check if stopped. + * @return true if the server was stopped (or never started) + */ + boolean isStopped() { server ? server.stopped : true } + + /** + * Get all current messages. + * @return the messages + */ + List getMessages() { server ? server.receivedEmail.collect { it } : [] } + + /** + * Get the number of sent messages. + * @return the number + */ + int getMessageCount() { server ? server.receivedEmailSize : 0 } +} Index: src/main/groovy/com/lemans/correspondence/forms/DomainFormFieldValidator.groovy =================================================================== diff -u --- src/main/groovy/com/lemans/correspondence/forms/DomainFormFieldValidator.groovy (revision 0) +++ src/main/groovy/com/lemans/correspondence/forms/DomainFormFieldValidator.groovy (revision 3ecf807f4027e53b3aaac2ffc17fc3617bfcc641) @@ -0,0 +1,81 @@ +package com.lemans.correspondence.forms + +import java.util.regex.PatternSyntaxException + +@SuppressWarnings(['CouldBeSwitchStatement']) +class DomainFormFieldValidator { + + static final int NAME_MIN = 3 + static final int NAME_MAX = 50 + static final String NAME_REGEX = /[a-zA-Z0-9]+/ + + static final String STRING = 'STRING' + static final String EMAIL = 'EMAIL' + static final String NUMBER = 'NUMBER' + static final String BOOLEAN = 'BOOLEAN' + + static final Set TYPES = [STRING, EMAIL, NUMBER, BOOLEAN].asImmutable() + + List validate(Map field) { + List errors = [] + validateName(field, errors) + validateType(field, errors) + validateMatches(field, errors) + if (field.min != null || field.max != null) { validateMinMax(field, errors) } + errors + } + + private validateName(Map field, List errors) { + String name = field.name?.trim() + if (!name) { errors << 'name is required' } + if (name?.size() < NAME_MIN) { errors << "name minimum length is $NAME_MIN" } + if (name?.size() > NAME_MAX) { errors << "name maximum length is $NAME_MAX" } + if (!(name ==~ NAME_REGEX)) { errors << 'name may only contain letters and numbers' } + } + + private validateType(Map field, List errors) { + String type = type(field) + if (!(type in TYPES)) { + errors << "type $type is not supported [${TYPES.join(',')}]" + } + } + + private validateMatches(Map field, List errors) { + String matches = field.matches?.trim() + String type = type(field) + if (matches) { + if (type == STRING) { + try { '' ==~ matches } + catch (PatternSyntaxException x) { errors << 'matches REGEX is invalid' } + } + else { errors << "matches is not supported for type $type" } + } + } + + private validateMinMax(Map field, List errors) { + String type = type(field) + if (type == STRING) { validateStringLengths(field, errors) } + else if (type == NUMBER) { validateMinMaxNumbers(field, errors) } + else { errors << "min and max are not supported for type $type" } + } + + private validateStringLengths(Map field, List errors) { + Integer min = field.min + Integer max = field.max + if (min != null && min < 1) { errors << 'min length must be > 0' } + if (max != null && max < 1) { errors << 'max length must be > 0' } + if (min != null && max != null && max < min) { + errors << 'max length must be >= min length' + } + } + + private validateMinMaxNumbers(Map field, List errors) { + Number min = field.min + Number max = field.max + if (min != null && max != null && max < min) { + errors << 'max must be >= min' + } + } + + private String type(Map field) { field.type?.trim() ?: STRING } +} Index: src/main/groovy/com/lemans/correspondence/forms/DomainFormFormTransformer.groovy =================================================================== diff -u --- src/main/groovy/com/lemans/correspondence/forms/DomainFormFormTransformer.groovy (revision 0) +++ src/main/groovy/com/lemans/correspondence/forms/DomainFormFormTransformer.groovy (revision 3ecf807f4027e53b3aaac2ffc17fc3617bfcc641) @@ -0,0 +1,49 @@ +package com.lemans.correspondence.forms + +import groovy.xml.MarkupBuilder + +class DomainFormFormTransformer { + + @SuppressWarnings('UnnecessaryCollectCall') + Map formFromXml(String xml) { + Node formNode = new XmlParser().parseText(xml) + List xmlFields = formNode.fields.collect { it*.attributes() } + List xmlRecipients = formNode.recipients.collect { it*.attributes() } + [fields: xmlFields[0].collect { createFieldFromXml(it) }, recipients: xmlRecipients[0]] + } + + String formToXml(Map form) { + StringWriter writer = new StringWriter() + new MarkupBuilder(writer).form { + fields { + form.fields.each { field(createSparselyPopulatedXmlField(it)) } + } + recipients { + form.recipients.each { recipient(name: it.name, email: it.email ) } + } + } + writer + } + + private Map createFieldFromXml(xmlField) { + Map field = [name: xmlField.name, type: xmlField.type] + if (xmlField.required) { field.required = xmlField.required.toBoolean() } + if (xmlField.matches) { field.matches = xmlField.matches } + if (xmlField.min != null) { + field.min = (field.type == 'STRING' ? xmlField.min.toInteger() : xmlField.min.toBigDecimal()) + } + if (xmlField.max != null) { + field.max = (xmlField.type == 'STRING' ? xmlField.max.toInteger() : xmlField.max.toBigDecimal()) + } + field + } + + private Map createSparselyPopulatedXmlField(Map field) { + Map sparseField = [name: field?.name, type: field?.type ?: DomainFormFieldValidator.STRING] + if (field?.required) { sparseField.required = field.required } + if (field?.matches) { sparseField.matches = field.matches } + if (field?.min != null) { sparseField.min = field.min.toString() } + if (field?.max != null) { sparseField.max = field.max.toString() } + sparseField + } +} Index: src/main/groovy/com/lemans/correspondence/forms/DomainFormFormValidator.groovy =================================================================== diff -u --- src/main/groovy/com/lemans/correspondence/forms/DomainFormFormValidator.groovy (revision 0) +++ src/main/groovy/com/lemans/correspondence/forms/DomainFormFormValidator.groovy (revision 3ecf807f4027e53b3aaac2ffc17fc3617bfcc641) @@ -0,0 +1,49 @@ +package com.lemans.correspondence.forms + +import com.lemans.correspondence.domain.forms.DomainForm +import org.springframework.validation.Errors + +class DomainFormFormValidator { + + private final DomainFormFieldValidator fieldValidator = new DomainFormFieldValidator() + + private final DomainFormRecipientValidator recipientValidator = new DomainFormRecipientValidator() + + /** + * Validates a DomainForm form definition, adding any errors to the global errors on the DomainForm. + * + * @param form Map + * @param domainForm + */ + void validate(Map form, DomainForm domainForm) { + Errors errors = domainForm.errors + validateFields(form.fields, errors) + validateRecipients(form.recipients, errors) + } + + private validateFields(List fields, Errors errors) { + if (fields) { + fields.eachWithIndex { Map field, Integer index -> + List msgs = fieldValidator.validate(field) + msgs.each { String msg -> reject "Field[$index] $msg", errors } + } + Map fieldsByName = fields.collectEntries { [(it.name): it] } + if (fields.size() > fieldsByName.size()) { reject 'Field name must be unique', errors } + } + else { reject 'At least 1 field is required', errors } + } + + private validateRecipients(List recipients, Errors errors) { + if (recipients) { + recipients.eachWithIndex { Map recipient, Integer index -> + List msgs = recipientValidator.validate(recipient) + msgs.each { String msg -> reject "Recipient[$index] $msg", errors } + } + Map recipientsByEmail = recipients.collectEntries { [(it.email): it] } + if (recipients.size() > recipientsByEmail.size()) { reject 'Recipient email must be unique', errors } + } + else { reject 'At least 1 recipient is required', errors } + } + + private reject(String msg, Errors errors) { errors.reject msg, msg } +} Index: src/main/groovy/com/lemans/correspondence/forms/DomainFormRecipientValidator.groovy =================================================================== diff -u --- src/main/groovy/com/lemans/correspondence/forms/DomainFormRecipientValidator.groovy (revision 0) +++ src/main/groovy/com/lemans/correspondence/forms/DomainFormRecipientValidator.groovy (revision 3ecf807f4027e53b3aaac2ffc17fc3617bfcc641) @@ -0,0 +1,29 @@ +package com.lemans.correspondence.forms + +import org.apache.commons.validator.routines.EmailValidator + +class DomainFormRecipientValidator { + + static final int NAME_MIN = 5 + static final int NAME_MAX = 50 + + List validate(Map recipient) { + List errors = [] + validateName(recipient, errors) + validateEmail(recipient, errors) + errors + } + + private validateName(Map recipient, List errors) { + String name = recipient.name?.trim() + if (!name) { errors << 'name is required' } + if (name?.size() < NAME_MIN) { errors << "name minimum length is $NAME_MIN" } + if (name?.size() > NAME_MAX) { errors << "name maximum length is $NAME_MAX" } + } + + private validateEmail(Map recipient, List errors) { + String email = recipient.email?.trim() + if (!email) { errors << 'email is required' } + if (!EmailValidator.instance.isValid(email)) { errors << 'email is invalid' } + } +} Index: src/main/groovy/com/lemans/correspondence/forms/DomainFormSubmissionValidator.groovy =================================================================== diff -u --- src/main/groovy/com/lemans/correspondence/forms/DomainFormSubmissionValidator.groovy (revision 0) +++ src/main/groovy/com/lemans/correspondence/forms/DomainFormSubmissionValidator.groovy (revision 3ecf807f4027e53b3aaac2ffc17fc3617bfcc641) @@ -0,0 +1,84 @@ +package com.lemans.correspondence.forms + +import org.apache.commons.validator.routines.EmailValidator + + +@SuppressWarnings(['CouldBeSwitchStatement']) +class DomainFormSubmissionValidator { + + /** + * Validates a DomainForm submission using the configured validation rules. + * + * @param fields configuration from the DomainForm + * @param submission Map with the form submission data + * + * @return List of errors which may be empty + */ + List validate(List fields, Map submission) { + List errors = [] + submission.entrySet().each { + if (!fields.find { Map field -> field.name == it.key } ) { + errors << reject(it.key, 'Unknown field') + } + } + fields.each { Map field -> + String name = field.name + String value = submission[name] + if (field.required && !value) { errors << reject(name, "$name is required") } + if (value) { + String type = field.type + if (type == DomainFormFieldValidator.STRING ) { validateString(errors, field, value) } + else if (type == DomainFormFieldValidator.EMAIL) { validateEmail(errors, field, value) } + else if (type == DomainFormFieldValidator.NUMBER ) { validateNumber(errors, field, value) } + else if (type == DomainFormFieldValidator.BOOLEAN) { validateBoolean(errors, field, value) } + } + } + errors + } + + private void validateString(List errors, Map field, String value) { + String matches = field.matches + if (matches && !(value ==~ matches)) { + errors << reject(field.name, "$field.name $value is invalid") + } + Integer min = field.min?.toInteger() + if (min != null && value.size() < min) { + errors << reject(field.name, "$field.name $value length must be at least $field.min") + } + Integer max = field.max?.toInteger() + if (max != null && value.size() > max) { + errors << reject(field.name, "$field.name $value length must be at most $field.max") + } + } + + private void validateEmail(List errors, Map field, String value) { + if (!EmailValidator.instance.isValid(value)) { + errors << reject(field.name, "$field.name $value is not a valid email address") + } + } + + private void validateNumber(List errors, Map field, String value) { + if (value.isBigDecimal()) { + BigDecimal number = value.toBigDecimal() + BigDecimal min = field.min?.toBigDecimal() + if (min != null && number < min) { + errors << reject(field.name, "$field.name $value must be at least $field.min") + } + BigDecimal max = field.max?.toBigDecimal() + if (max != null && number > max) { + errors << reject(field.name, "$field.name $value must be at most $field.max") + } + } + else { errors << reject(field.name, "$field.name $value must be a number") } + } + + private void validateBoolean(List errors, Map field, String value) { + if (value != 'true' && value != 'false') { + errors << reject(field.name, "$field.name must be true or false") + } + } + + private Map reject(String name, String message) { + [type: 'error', field: name, text: message] + } +} Index: src/main/groovy/com/lemans/correspondence/services/DomainFormManagerService.groovy =================================================================== diff -u --- src/main/groovy/com/lemans/correspondence/services/DomainFormManagerService.groovy (revision 0) +++ src/main/groovy/com/lemans/correspondence/services/DomainFormManagerService.groovy (revision 3ecf807f4027e53b3aaac2ffc17fc3617bfcc641) @@ -0,0 +1,95 @@ +package com.lemans.correspondence.services + +import com.lemans.correspondence.domain.domain.DomainRepository +import com.lemans.correspondence.domain.forms.DomainForm +import com.lemans.correspondence.domain.forms.DomainFormRepository +import com.lemans.correspondence.forms.DomainFormFormTransformer +import com.lemans.correspondence.forms.DomainFormFormValidator +import com.lemans.services.LemansManager +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@Transactional +class DomainFormManagerService extends LemansManager { + + private final DomainFormFormTransformer transformer = new DomainFormFormTransformer() + + private final DomainFormFormValidator validator = new DomainFormFormValidator() + + @Autowired + DomainFormRepository domainFormRepository + + @Autowired + DomainRepository domainRepository + + /** + * Creates a new DomainForm. + * + * @param values + * @param username + * @param domainId + * + * @return DomainForm which may contain errors or be null + */ + DomainForm createDomainForm(DomainForm domainForm, String username, Integer domainId) { + domainForm.domain = domainRepository.findById(domainId) + domainForm.formDetailXml = transformer.formToXml(domainForm.form) + domainForm.validate() + validator.validate(domainForm.form, domainForm) + domainFormRepository.saveOrDiscardDomain(domainForm, username) + } + + /** + * Updates an existing DomainForm. + * + * @param values + * @param formKey + * @param username + * @param domainId + * + * @return DomainForm which may errors or be null + */ + DomainForm updateDomainForm(Map values, String formKey, String username, Integer domainId) { + DomainForm domainForm = findDomainForm(formKey, domainId) + if (domainForm) { + applyValuesToDomain(values, domainForm) + Map form = values.form + if (form) { + domainForm.formDetailXml = transformer.formToXml(form) + validator.validate(form, domainForm) + } + domainForm.validate() + domainFormRepository.saveOrDiscardDomain(domainForm, username) + } + domainForm + } + + /** + * Soft deletes a DomainForm. + * + * @param formKey + * @param username + * @param domainId + * + * @return DomainForm which may errors or be null + */ + DomainForm deleteDomainForm(String formKey, String username, Integer domainId) { + DomainForm form = findDomainForm(formKey, domainId) + if (form) { domainFormRepository.softDelete(form, username) } + form + } + + /** + * Finds an active or inactive DomainForm by formKey and domainId. + * + * @param formKey + * @param domainId + * + * @return DomainForm or null + */ + DomainForm findDomainForm(String formKey, Integer domainId) { + domainFormRepository.findByFormKeyAndDomain(formKey, domainRepository.findById(domainId)) + } +} Index: src/main/groovy/com/lemans/correspondence/services/DomainFormService.groovy =================================================================== diff -u --- src/main/groovy/com/lemans/correspondence/services/DomainFormService.groovy (revision 0) +++ src/main/groovy/com/lemans/correspondence/services/DomainFormService.groovy (revision 3ecf807f4027e53b3aaac2ffc17fc3617bfcc641) @@ -0,0 +1,78 @@ +package com.lemans.correspondence.services + +import com.lemans.correspondence.forms.DomainFormFormTransformer +import com.lemans.services.LemansService +import org.springframework.stereotype.Service + +import javax.sql.rowset.serial.SerialClob +import java.sql.Clob + +@Service +class DomainFormService extends LemansService { + + private final DomainFormFormTransformer transformer = new DomainFormFormTransformer() + + /** + * Finds a form for a domain by formKey. + * + * @param criteria containing domainId and formKey + * + * @return Map containing form data + */ + Map findForm(Map criteria) { + criteria.sorting = criteria.sorting ?: 'formName' + Map data = dqx(criteria).executeFrom('dbo.vwDomainForm', formClauses(criteria)) + transformDetailXmlClobToMap(data?.results) + data?.results ? data?.results[0] : [:] + } + + /** + * Finds forms for a domain. + * + * @param criteria containing domainId + * + * @return Map containing results and totalRecords + */ + Map findForms(Map criteria) { + criteria.sorting = criteria.sorting ?: 'formName' + Map data = dqx(criteria).executeFrom('dbo.vwDomainForm', ['domainId = :domainId']) + transformDetailXmlClobToMap(data?.results) + data + } + + /** + * Finds all the supported formTypes. + * + * @return List of formTypes + */ + List allFormTypes() { + sql().rows('SELECT * FROM dbo.vwDomainFormType') + } + + /** + * Finds a formType by id. + * + * @param id + * + * @return Map with formType data + */ + Map formTypeById(Integer id) { + sql().firstRow('SELECT * FROM dbo.vwDomainFormType WHERE domainFormTypeId = ?', id) + } + + private List formClauses(Map criteria) { + List clauses = ['domainId = :domainId'] + if (criteria.formKey) { clauses << 'formKey = :formKey' } + if (criteria.id) { clauses << 'domainFormId = :id' } + clauses + } + + private void transformDetailXmlClobToMap(List forms) { + forms.each { Map form -> + Clob clob = new SerialClob(form.formDetailXml.toCharArray()) + String xml = clob.characterStream.text + form.form = transformer.formFromXml(xml) + form.remove 'formDetailXml' + } + } +} Index: src/main/groovy/com/lemans/correspondence/services/EmailService.groovy =================================================================== diff -u --- src/main/groovy/com/lemans/correspondence/services/EmailService.groovy (revision 0) +++ src/main/groovy/com/lemans/correspondence/services/EmailService.groovy (revision 3ecf807f4027e53b3aaac2ffc17fc3617bfcc641) @@ -0,0 +1,56 @@ +package com.lemans.correspondence.services + +import groovy.text.markup.MarkupTemplateEngine +import groovy.text.markup.TemplateConfiguration +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.mail.MailSender +import org.springframework.stereotype.Service + +import javax.mail.internet.InternetAddress +import javax.mail.internet.MimeMessage +import javax.mail.internet.MimeMessage.RecipientType + +@Service +@SuppressWarnings(['UnnecessarySetter']) +class EmailService { + + @Autowired + MailSender mailSender + + def formTemplate = ''' + html { + head { + title('formSubmit') + } + body{ + h2("$formName form:") + table(border:1){ + tr{ + th("FieldName") + th("FieldData") + } + formData.each { data -> + tr{ + td("$data.key") + td("$data.value") + } + } + } + + } + } + ''' + + def sendFormSubmitEmail(Map formData, Map emailData) { + TemplateConfiguration config = new TemplateConfiguration() + MarkupTemplateEngine engine = new MarkupTemplateEngine(config) + Map templateData = [formData: formData, formName: emailData.subject] + String body = engine.createTemplate(formTemplate).make(templateData).writeTo(new StringWriter()) + MimeMessage mimeMessage = mailSender?.createMimeMessage() + mimeMessage?.setRecipient(RecipientType.TO, new InternetAddress(emailData?.to)) + mimeMessage?.setFrom(new InternetAddress(emailData?.from)) + mimeMessage?.setSubject(emailData?.subject) + mimeMessage?.setContent(body, 'text/html') + mailSender?.send(mimeMessage) + } +} Index: src/main/groovy/com/lemans/correspondence/services/FormSubmitService.groovy =================================================================== diff -u --- src/main/groovy/com/lemans/correspondence/services/FormSubmitService.groovy (revision 0) +++ src/main/groovy/com/lemans/correspondence/services/FormSubmitService.groovy (revision 3ecf807f4027e53b3aaac2ffc17fc3617bfcc641) @@ -0,0 +1,67 @@ +package com.lemans.correspondence.services + +import com.lemans.correspondence.forms.DomainFormSubmissionValidator +import com.lemans.services.LemansService +import groovy.xml.MarkupBuilder +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.stereotype.Service + +@Service +class FormSubmitService extends LemansService { + + @Autowired + EmailService emailService + + Object correspondenceEmailDomain + + private final DomainFormSubmissionValidator validator = new DomainFormSubmissionValidator() + + static final String FORM_INSERT_SQL = ''' + EXEC dbo.spInsertFormRequest + @domainFormId = :domainFormId, + @domainId = :domainId, + @userId = :userId, + @emailAddress = :emailAddress, + @requestXml = :xml + ''' + + List saveSubmittedForm(Map domainForm, Map formData) { + List errors = validator.validate(domainForm.form.fields, formData) + if (errors) { return errors } + + String formXml = submitFromToXML(formData) + // store form data to database + Map criteria = [ + domainFormId: domainForm.domainFormId, + domainId: domainForm.domainId, + emailAddress: formData.emailAddress, + xml: formXml + ] + + List response = sql().rows(criteria, FORM_INSERT_SQL) + //send formData as email to domainForm recipients. + Map emailData = constructEmailData(response[0], domainForm) + emailService.sendFormSubmitEmail(formData, emailData) + + errors + } + + private Map constructEmailData(Map dbResponse, Map domainForm) { + [ + to: dbResponse.responderGuid + '@' + correspondenceEmailDomain, + from: dbResponse.requesterGuid + '@' + correspondenceEmailDomain, + subject: domainForm.formName + ] + + } + + private String submitFromToXML(domainForm) { + StringWriter writer = new StringWriter() + new MarkupBuilder(writer).form { + domainForm.each { key, value -> + "$key"(value) + } + } + writer.toString() + } +} Index: src/main/resources/application.groovy =================================================================== diff -u --- src/main/resources/application.groovy (revision 0) +++ src/main/resources/application.groovy (revision 3ecf807f4027e53b3aaac2ffc17fc3617bfcc641) @@ -0,0 +1,9 @@ +import com.lemans.services.Env + +if (Env.getCurrent() == Env.PRODUCTION) { + spring.datasource.'jndi-name'='java:comp/env/jdbc/appSecurity' +} else { + spring.datasource.url='jdbc:sqlserver://dev-dbprod02vm;databaseName=AppSecurity' + spring.datasource.username='appsecurity_user' + spring.datasource.password='DevPassword1' +} Index: src/main/resources/application.properties =================================================================== diff -u --- src/main/resources/application.properties (revision 0) +++ src/main/resources/application.properties (revision 3ecf807f4027e53b3aaac2ffc17fc3617bfcc641) @@ -0,0 +1,23 @@ +spring.application.name=correspondence-service +server.servlet.context-path=/correspondence-service + +spring.datasource.driverClassName=com.microsoft.sqlserver.jdbc.SQLServerDriver + +spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.SQLServer2008Dialect +spring.jpa.hibernate.ddl-auto = none + +#logging sql +logging.level.com.microsoft.sqlserver.jdbc=debug + +logging.level.org.hibernate.SQL=DEBUG +logging.level.org.hibernate.type=TRACE + +#logging.level.root=DEBUG + +spring.jpa.properties.hibernate.show_sql=true +spring.jpa.properties.hibernate.use_sql_comments=true +spring.jpa.properties.hibernate.format_sql=true + +spring.main.allow-bean-definition-overriding=true \ No newline at end of file Index: src/main/resources/messages.properties =================================================================== diff -u --- src/main/resources/messages.properties (revision 0) +++ src/main/resources/messages.properties (revision 3ecf807f4027e53b3aaac2ffc17fc3617bfcc641) @@ -0,0 +1 @@ +javax.validation.constraints.Size.message=Invalid size for input: ${validatedValue} \ No newline at end of file Index: src/main/resources/resources.groovy =================================================================== diff -u --- src/main/resources/resources.groovy (revision 0) +++ src/main/resources/resources.groovy (revision 3ecf807f4027e53b3aaac2ffc17fc3617bfcc641) @@ -0,0 +1,43 @@ +import com.lemans.correspondence.dumbster.Dumbster +import com.lemans.services.Env +import org.apache.http.impl.client.DefaultHttpClient +import org.apache.http.impl.conn.PoolingClientConnectionManager +import org.springframework.mail.javamail.JavaMailSenderImpl + + +beans { + + if (Env.getCurrent() == Env.PRODUCTION) { + authServiceContext(org.springframework.jndi.JndiObjectFactoryBean) { + jndiName = 'java:comp/env/authServiceContext' + } + correspondenceEmailDomain(org.springframework.jndi.JndiObjectFactoryBean) { + jndiName = 'java:comp/env/correspondenceEmailDomain' + } + emailHost(org.springframework.jndi.JndiObjectFactoryBean) { + jndiName = 'java:comp/env/emailHost' + } + } else { + authServiceContext(String, 'http://services1.dev.lemanscorp.com/auth-service/verifyRequest') + correspondenceEmailDomain(String, 'test.com') + emailHost(String, 'localhost') + } + + httpConnectionManager(PoolingClientConnectionManager) { + setMaxPerRoute(50) + setDefaultMaxPerRoute(100) + setMaxTotal(200) + } + httpClient(DefaultHttpClient, httpConnectionManager) + + mailSender(JavaMailSenderImpl) { + host = ref('emailHost') + } + + if (Env.getCurrent() != Env.PRODUCTION) { + dumbster(Dumbster) { bean -> + bean.initMethod = 'start' + bean.destroyMethod = 'stop' + } + } +} Index: src/test/groovy/com/lemans/correspondence/email/EmailServiceIntegrationSpec.groovy =================================================================== diff -u --- src/test/groovy/com/lemans/correspondence/email/EmailServiceIntegrationSpec.groovy (revision 0) +++ src/test/groovy/com/lemans/correspondence/email/EmailServiceIntegrationSpec.groovy (revision 3ecf807f4027e53b3aaac2ffc17fc3617bfcc641) @@ -0,0 +1,41 @@ +package com.lemans.correspondence.email + +import com.dumbster.smtp.SmtpMessage +import com.lemans.correspondence.CorrespondenceApplication +import com.lemans.correspondence.dumbster.Dumbster +import com.lemans.correspondence.services.EmailService +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import spock.lang.Specification + +@SpringBootTest(classes = CorrespondenceApplication) +class EmailServiceIntegrationSpec extends Specification { + + @Autowired + EmailService emailService + + @Autowired + Dumbster dumbster + + def setup() { dumbster.reset() } + + def 'can send a test email'() { + given: + Map formData = [stuff: 'yipee', moreStuff: 'kaiya'] + Map emailData = [to: 'handler@lemans.com', from: 'initiator@lemans.com', subject: 'contactUs'] + + when: + emailService.sendFormSubmitEmail(formData, emailData) + SmtpMessage msg = dumbster.messages[0] + + then: + dumbster.messageCount == 1 + msg.subject == 'contactUs' + msg.tos == ['handler@lemans.com'] + msg.froms == ['initiator@lemans.com'] + msg.body.contains 'stuff' + msg.body.contains 'yipee' + msg.body.contains 'moreStuff' + msg.body.contains 'kaiya' + } +} Index: src/test/groovy/com/lemans/correspondence/forms/DomainFormFuncSpec.groovy =================================================================== diff -u --- src/test/groovy/com/lemans/correspondence/forms/DomainFormFuncSpec.groovy (revision 0) +++ src/test/groovy/com/lemans/correspondence/forms/DomainFormFuncSpec.groovy (revision 3ecf807f4027e53b3aaac2ffc17fc3617bfcc641) @@ -0,0 +1,162 @@ +package com.lemans.correspondence.forms + +import com.lemans.correspondence.testing.CorrespondenceFuncSpec +import spock.lang.Shared + + +class DomainFormFuncSpec extends CorrespondenceFuncSpec { + + @Override + String resourceName() { 'form' } + + @Shared + private static final String KEY = 'zzz_' + new Date() + + + def 'can create a valid DomainForm'() { + given: + int domain = 18 + int typeId = 1 + String name = 'Thor Contact Us' + path(domain: domain) + String json = +""" +{ + "domainFormTypeId": "$typeId", + "formName": "$name", + "formKey": "$KEY", + "form": { + "fields": [ + { "name": "firstName", "required": false }, + { "name": "lastName", "required": true, "matches": "[A-Za-z0-9]+", "min": 3, "max": 50 }, + { "name": "emailAddress", "type": "EMAIL" } + ], + "recipients": [ + { "name": "Buddy Guy", "email": "bguy@parts-unltd.com" }, + { "name": "Jonny Lang", "email": "jlang@parts-unltd.com" } + ] + } +} +""" + ok() + + when: + post(json) + + then: + with(payload.results) { + domainId == domain + domainFormTypeId == typeId + formName == name + formKey == KEY + form.fields.size() == 3 + form.recipients.size() == 2 + } + } + + def 'can NOT create an invalid DomainForm'() { + given: + int domain = 18 + path(domain: domain) + String json = '''{ "domainFormTypeId": 1, "formKey": "whatever", "form": { } }''' + invalid() + + when: + post(json) + + then: + with(payload.messages) { + it[0].type == 'error' + it[0].text == 'At least 1 field is required' + it[1].type == 'error' + it[1].text == 'At least 1 recipient is required' + size() == 2 + } + } + + def 'can NOT create a DomainForm with duplicate field names'() { + given: + int domain = 18 + path(domain: domain) + String json = + ''' +{ + "domainFormTypeId": 1, + "formKey": "whatever", + "formName": "what ever", + "form": { + "fields": [ + { "name": "firstName" }, + { "name": "firstName"} + ], + "recipients": [ + { "name": "Buddy Guy", "email": "bguy@parts-unltd.com" }, + { "name": "Jonny Lang", "email": "bguy@parts-unltd.com" } + ] + } +} +''' + invalid() + + when: + post(json) + + then: + with(payload.messages) { + it[0].type == 'error' + it[0].text == 'Field name must be unique' + it[1].type == 'error' + it[1].text == 'Recipient email must be unique' + size() == 2 + } + } + + def 'can NOT update an invalid DomainForm'() { + given: + path(domain: 18, KEY) + String json = '{ "formName": "01234567890123456789012345678901234567890123456789______" }' + invalid() + + when: + put(json) + + then: + with(payload.messages[0]) { + type == 'error' + field == 'formName' + text.contains 'exceeds the maximum size' + } + payload.messages.size() == 1 + } + + def 'can update a valid DomainForm'() { + given: + int domain = 18 + String name = 'XYZ_' + new Date() + path(domain: domain, KEY) + String json = """{ "formName": "$name" }""" + ok() + + when: + put(json) + + then: + with(payload.results) { + formName == name + formKey == KEY + domainId == domain + } + } + + def 'can delete a DomainForm'() { + given: + path(domain: 18, KEY) + ok() + + when: + delete() + + then: + !payload + } +} Index: src/test/groovy/com/lemans/correspondence/forms/DomainFormQueryFuncSpec.groovy =================================================================== diff -u --- src/test/groovy/com/lemans/correspondence/forms/DomainFormQueryFuncSpec.groovy (revision 0) +++ src/test/groovy/com/lemans/correspondence/forms/DomainFormQueryFuncSpec.groovy (revision 3ecf807f4027e53b3aaac2ffc17fc3617bfcc641) @@ -0,0 +1,63 @@ +package com.lemans.correspondence.forms + +import com.lemans.correspondence.testing.CorrespondenceFuncSpec + + +class DomainFormQueryFuncSpec extends CorrespondenceFuncSpec { + + @Override + String resourceName() { 'form' } + + def 'can find all the forms for a domain'() { + given: + int domainId = 18 + path(domain: domainId) + ok() + + when: + get() + + then: + with(payload.results) { + size() >= 1 + domainFormId.every { it != null } + domainId.every { it == domainId } + formKey.every { it != null } + domainFormTypeId.every { it != null } + form.every { it } + } + payload.meta.totalRecords >= 1 + } + + def 'can find a form for a domain by formKey'() { + given: + int theDomainId = 18 + String key = 'TESST123' + path(domain: theDomainId, "$key") + ok() + + when: + get() + + then: + with(payload.results) { + domainId == theDomainId + formKey == key + form.fields.size() + form.recipients.size() + } + } + + def 'can NOT find a form for a domain by id that does not exist'() { + given: + path(domain: 18, '_ffg_') + notFound() + + when: + get() + + then: + !payload + } +} + Index: src/test/groovy/com/lemans/correspondence/forms/DomainFormTypeFuncSpec.groovy =================================================================== diff -u --- src/test/groovy/com/lemans/correspondence/forms/DomainFormTypeFuncSpec.groovy (revision 0) +++ src/test/groovy/com/lemans/correspondence/forms/DomainFormTypeFuncSpec.groovy (revision 3ecf807f4027e53b3aaac2ffc17fc3617bfcc641) @@ -0,0 +1,51 @@ +package com.lemans.correspondence.forms + +import com.lemans.correspondence.testing.CorrespondenceFuncSpec + + +class DomainFormTypeFuncSpec extends CorrespondenceFuncSpec { + + @Override + String resourceName() { 'form/type' } + + def 'can find all the formTypes'() { + given: + path() + ok() + + when: + get() + + then: + with(payload) { + results.size() >= 1 + } + } + + def 'can find a formType by id'() { + given: + path('1') + ok() + + when: + get() + + then: + with(payload) { + results.domainFormTypeId == 1 + results.typeName == 'Contact Us' + } + } + + def 'can NOT find a formType by id that does not exist'() { + given: + path('-1') + notFound() + + when: + get() + + then: + !payload + } +} Index: src/test/groovy/com/lemans/correspondence/forms/FormSubmitFuncSpec.groovy =================================================================== diff -u --- src/test/groovy/com/lemans/correspondence/forms/FormSubmitFuncSpec.groovy (revision 0) +++ src/test/groovy/com/lemans/correspondence/forms/FormSubmitFuncSpec.groovy (revision 3ecf807f4027e53b3aaac2ffc17fc3617bfcc641) @@ -0,0 +1,72 @@ +package com.lemans.correspondence.forms + +import com.lemans.correspondence.testing.CorrespondenceFuncSpec + +/** + * Created by arajaraman on 2/18/2016. + */ +class FormSubmitFuncSpec extends CorrespondenceFuncSpec { + + @Override + String resourceName() { 'submit' } + + private final static String UNKNOWN_USER = 'domainForm is unknown' + + def 'can not create a DomainForm with invalid formKey'() { + given: + int domain = 18 + String formKey = 'AVBC' + path(domain: domain, form: formKey) + String json = '''{ "form_contactName": "test", "form_emailAddress": "att.com" }''' + + notFound() + + when: + post(json) + + then: + with(payload) { + messages.size() == 1 + messages[0].type == 'error' + messages[0].text == UNKNOWN_USER + } + } + + def 'can not create a DomainForm with invalid data'() { + given: + int domain = 5 + String formKey = 'MOOSE_CU' + path(domain: domain, form: formKey) + String json = '''{ "form_contactName": "test", "form_emailAddress": "att.com" }''' + + invalid() + + when: + post(json) + + then: + with(payload) { + messages.size() == 1 + messages[0].type == 'error' + messages[0].text == 'emailAddress att.com is not a valid email address' + messages[0].field == 'emailAddress' + } + } + + + def 'can create a DomainForm with valid data'() { + given: + int domain = 5 + String formKey = 'MOOSE_CU' + path(domain: domain, form: formKey) + String json = '''{ "form_1_emailAddress": "att@tt.com", "form_10_contactName": "test", "form_contactMessage": "test" }''' + + ok() + + when: + post(json) + + then: + !payload + } +} Index: src/test/groovy/com/lemans/correspondence/security/TokenVerifierFuncSpec.groovy =================================================================== diff -u --- src/test/groovy/com/lemans/correspondence/security/TokenVerifierFuncSpec.groovy (revision 0) +++ src/test/groovy/com/lemans/correspondence/security/TokenVerifierFuncSpec.groovy (revision 3ecf807f4027e53b3aaac2ffc17fc3617bfcc641) @@ -0,0 +1,34 @@ +package com.lemans.correspondence.security + +import com.lemans.correspondence.testing.CorrespondenceFuncSpec + +class TokenVerifierFuncSpec extends CorrespondenceFuncSpec { + + def 'can act with a valid security token'() { + given: + System.setProperty('ignoreToken', 'true') + domain = null + path() + expectedStatusCode = 200 + + when: + get() + + then: + payload.results == 'correspondence-service' + } + + def 'can NOT act without a valid security token'() { + given: + System.setProperty('ignoreToken', '') + domain = null + path() + expectedStatusCode = 401 + + when: + get() + + then: + payload.messages[0].text == 'no credentials' + } +} Index: src/test/groovy/com/lemans/correspondence/testing/CorrespondenceFuncSpec.groovy =================================================================== diff -u --- src/test/groovy/com/lemans/correspondence/testing/CorrespondenceFuncSpec.groovy (revision 0) +++ src/test/groovy/com/lemans/correspondence/testing/CorrespondenceFuncSpec.groovy (revision 3ecf807f4027e53b3aaac2ffc17fc3617bfcc641) @@ -0,0 +1,22 @@ +package com.lemans.correspondence.testing + +import com.lemans.correspondence.CorrespondenceApplication +import com.lemans.testing.LemansApiFunctionalSpec +import groovyx.net.http.HTTPBuilder +import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.test.context.SpringBootTest + +@SpringBootTest(classes = CorrespondenceApplication, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +abstract class CorrespondenceFuncSpec extends LemansApiFunctionalSpec { + + @Override + final String serviceName() { 'correspondence-service' } + + @Value('${local.server.port}') + Integer serverPort + + def setup() { + port = serverPort + http = new HTTPBuilder(url()) + } +}