├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── pom.xml ├── settings.gradle └── src └── main ├── java └── app │ ├── Application.java │ ├── book │ ├── Book.java │ ├── BookController.java │ └── BookDao.java │ ├── index │ └── IndexController.java │ ├── login │ └── LoginController.java │ ├── user │ ├── User.java │ ├── UserController.java │ └── UserDao.java │ └── util │ ├── Filters.java │ ├── JsonUtil.java │ ├── MessageBundle.java │ ├── Path.java │ ├── RequestUtil.java │ └── ViewUtil.java └── resources ├── localization ├── messages_de.properties └── messages_en.properties ├── public ├── img │ ├── english.png │ ├── favicon.png │ ├── german.png │ └── logo.png └── main.css ├── velocity ├── book │ ├── all.vm │ └── one.vm ├── index │ └── index.vm ├── layout.vm ├── login │ └── login.vm └── notFound.vm └── velocityconfig └── velocity_implicit.vm /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | 3 | ### Windows ### 4 | # Windows image file caches 5 | Thumbs.db 6 | ehthumbs.db 7 | 8 | # Folder config file 9 | Desktop.ini 10 | 11 | # Recycle Bin used on file shares 12 | $RECYCLE.BIN/ 13 | 14 | # Windows Installer files 15 | *.cab 16 | *.msi 17 | *.msm 18 | *.msp 19 | 20 | # Windows shortcuts 21 | *.lnk 22 | 23 | 24 | ### OSX ### 25 | .DS_Store 26 | .AppleDouble 27 | .LSOverride 28 | 29 | # Icon must end with two \r 30 | Icon 31 | 32 | 33 | # Thumbnails 34 | ._* 35 | 36 | # Files that might appear in the root of a volume 37 | .DocumentRevisions-V100 38 | .fseventsd 39 | .Spotlight-V100 40 | .TemporaryItems 41 | .Trashes 42 | .VolumeIcon.icns 43 | 44 | # Directories potentially created on remote AFP share 45 | .AppleDB 46 | .AppleDesktop 47 | Network Trash Folder 48 | Temporary Items 49 | .apdisk 50 | 51 | 52 | ### Intellij ### 53 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 54 | 55 | *.iml 56 | 57 | ## Directory-based project format: 58 | .idea/ 59 | # if you remove the above rule, at least ignore the following: 60 | 61 | # User-specific stuff: 62 | # .idea/workspace.xml 63 | # .idea/tasks.xml 64 | # .idea/dictionaries 65 | 66 | # Sensitive or high-churn files: 67 | # .idea/dataSources.ids 68 | # .idea/dataSources.xml 69 | # .idea/sqlDataSources.xml 70 | # .idea/dynamic.xml 71 | # .idea/uiDesigner.xml 72 | 73 | # Gradle: 74 | # .idea/gradle.xml 75 | # .idea/libraries 76 | 77 | # Mongo Explorer plugin: 78 | # .idea/mongoSettings.xml 79 | 80 | ## File-based project format: 81 | *.ipr 82 | *.iws 83 | 84 | ## Plugin-specific files: 85 | 86 | # IntelliJ 87 | /out/ 88 | 89 | # mpeltonen/sbt-idea plugin 90 | .idea_modules/ 91 | 92 | # JIRA plugin 93 | atlassian-ide-plugin.xml 94 | 95 | # Crashlytics plugin (for Android Studio and IntelliJ) 96 | com_crashlytics_export_strings.xml 97 | crashlytics.properties 98 | crashlytics-build.properties 99 | 100 | 101 | ### Java ### 102 | *.class 103 | 104 | # Mobile Tools for Java (J2ME) 105 | .mtj.tmp/ 106 | 107 | # Package Files # 108 | *.jar 109 | *.war 110 | *.ear 111 | 112 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 113 | hs_err_pid* 114 | target 115 | 116 | 117 | # Created by https://www.gitignore.io/api/gradle 118 | 119 | ### Gradle ### 120 | .gradle 121 | build/ 122 | 123 | # Ignore Gradle GUI config 124 | gradle-app.setting 125 | 126 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 127 | !gradle-wrapper.jar 128 | 129 | # Cache of project 130 | .gradletasknamecache 131 | 132 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 133 | # gradle/wrapper/gradle-wrapper.properties 134 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2015 David Åse 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spark-basic-structure 2 | This is an example of one possible way of structuring a Spark application 3 | 4 | The application has filters, controllers, views, authentication, localization, error handling, and more. 5 | It contains the source code for the tutorial found at https://sparktutorials.github.io/2016/06/10/spark-basic-structure.html 6 | 7 | ## Critique welcome 8 | If you find anything you disagree with, please feel free to create an issue. 9 | 10 | ## Screenshot 11 | ![Application Screenshot](https://sparktutorials.github.io/img/posts/sparkBasicStructure/screenshot.png) 12 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | apply plugin: 'maven' 3 | apply plugin: 'application' 4 | 5 | group = 'spark-basic-structure' 6 | version = '1.0-SNAPSHOT' 7 | 8 | description = """""" 9 | 10 | sourceCompatibility = 1.8 11 | targetCompatibility = 1.8 12 | 13 | mainClassName = "app.Application" 14 | 15 | repositories { 16 | jcenter() 17 | maven { url "http://repo.maven.apache.org/maven2" } 18 | } 19 | dependencies { 20 | compile group: 'com.sparkjava', name: 'spark-core', version:'2.5' 21 | compile group: 'com.sparkjava', name: 'spark-debug-tools', version:'0.5' 22 | compile group: 'com.sparkjava', name: 'spark-template-velocity', version:'2.3' 23 | compile group: 'org.slf4j', name: 'slf4j-simple', version:'1.7.13' 24 | compile group: 'org.projectlombok', name: 'lombok', version:'1.16.6' 25 | compile group: 'com.fasterxml.jackson.core', name: 'jackson-core', version:'2.5.1' 26 | compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version:'2.5.1' 27 | compile group: 'com.google.guava', name: 'guava', version:'19.0' 28 | compile group: 'org.mindrot', name: 'jbcrypt', version:'0.3m' 29 | } 30 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tipsy/spark-basic-structure/5af6b78e0869dd2b2f014274ef731a2468ed0b8e/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Jun 12 13:04:02 EEST 2016 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.13-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | spark-basic-structure 8 | spark-basic-structure 9 | 1.0-SNAPSHOT 10 | 11 | 12 | 13 | org.apache.maven.plugins 14 | maven-compiler-plugin 15 | 16 | 1.8 17 | 1.8 18 | 19 | 20 | 21 | org.codehaus.mojo 22 | exec-maven-plugin 23 | 1.2.1 24 | 25 | app.Application 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | com.sparkjava 36 | spark-core 37 | 2.5 38 | 39 | 40 | com.sparkjava 41 | spark-debug-tools 42 | 0.5 43 | 44 | 45 | com.sparkjava 46 | spark-template-velocity 47 | 2.3 48 | 49 | 50 | org.slf4j 51 | slf4j-simple 52 | 1.7.13 53 | 54 | 55 | org.projectlombok 56 | lombok 57 | 1.16.6 58 | 59 | 60 | com.fasterxml.jackson.core 61 | jackson-core 62 | 2.5.1 63 | 64 | 65 | com.fasterxml.jackson.core 66 | jackson-databind 67 | 2.5.1 68 | 69 | 70 | com.google.guava 71 | guava 72 | 19.0 73 | 74 | 75 | org.mindrot 76 | jbcrypt 77 | 0.3m 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'spark-basic-structure' 2 | -------------------------------------------------------------------------------- /src/main/java/app/Application.java: -------------------------------------------------------------------------------- 1 | package app; 2 | 3 | import app.book.*; 4 | import app.index.*; 5 | import app.login.*; 6 | import app.user.*; 7 | import app.util.*; 8 | import static spark.Spark.*; 9 | import static spark.debug.DebugScreen.*; 10 | 11 | public class Application { 12 | 13 | // Declare dependencies 14 | public static BookDao bookDao; 15 | public static UserDao userDao; 16 | 17 | public static void main(String[] args) { 18 | 19 | // Instantiate your dependencies 20 | bookDao = new BookDao(); 21 | userDao = new UserDao(); 22 | 23 | // Configure Spark 24 | port(4567); 25 | staticFiles.location("/public"); 26 | staticFiles.expireTime(600L); 27 | enableDebugScreen(); 28 | 29 | // Set up before-filters (called before each get/post) 30 | before("*", Filters.addTrailingSlashes); 31 | before("*", Filters.handleLocaleChange); 32 | 33 | // Set up routes 34 | get(Path.Web.INDEX, IndexController.serveIndexPage); 35 | get(Path.Web.BOOKS, BookController.fetchAllBooks); 36 | get(Path.Web.ONE_BOOK, BookController.fetchOneBook); 37 | get(Path.Web.LOGIN, LoginController.serveLoginPage); 38 | post(Path.Web.LOGIN, LoginController.handleLoginPost); 39 | post(Path.Web.LOGOUT, LoginController.handleLogoutPost); 40 | get("*", ViewUtil.notFound); 41 | 42 | //Set up after-filters (called after each get/post) 43 | after("*", Filters.addGzipHeader); 44 | 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/app/book/Book.java: -------------------------------------------------------------------------------- 1 | package app.book; 2 | 3 | import lombok.*; 4 | 5 | @Value // All fields are private and final. Getters (but not setters) are generated (https://projectlombok.org/features/Value.html) 6 | public class Book { 7 | String title; 8 | String author; 9 | String isbn; 10 | 11 | public String getMediumCover() { 12 | return "http://covers.openlibrary.org/b/isbn/" + this.isbn + "-M.jpg"; 13 | } 14 | 15 | public String getLargeCover() { 16 | return "http://covers.openlibrary.org/b/isbn/" + this.isbn + "-L.jpg"; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/app/book/BookController.java: -------------------------------------------------------------------------------- 1 | package app.book; 2 | 3 | import app.login.*; 4 | import app.util.*; 5 | import spark.*; 6 | import java.util.*; 7 | import static app.Application.bookDao; 8 | import static app.util.JsonUtil.*; 9 | import static app.util.RequestUtil.*; 10 | 11 | public class BookController { 12 | 13 | public static Route fetchAllBooks = (Request request, Response response) -> { 14 | LoginController.ensureUserIsLoggedIn(request, response); 15 | if (clientAcceptsHtml(request)) { 16 | HashMap model = new HashMap<>(); 17 | model.put("books", bookDao.getAllBooks()); 18 | return ViewUtil.render(request, model, Path.Template.BOOKS_ALL); 19 | } 20 | if (clientAcceptsJson(request)) { 21 | return dataToJson(bookDao.getAllBooks()); 22 | } 23 | return ViewUtil.notAcceptable.handle(request, response); 24 | }; 25 | 26 | public static Route fetchOneBook = (Request request, Response response) -> { 27 | LoginController.ensureUserIsLoggedIn(request, response); 28 | if (clientAcceptsHtml(request)) { 29 | HashMap model = new HashMap<>(); 30 | Book book = bookDao.getBookByIsbn(getParamIsbn(request)); 31 | model.put("book", book); 32 | return ViewUtil.render(request, model, Path.Template.BOOKS_ONE); 33 | } 34 | if (clientAcceptsJson(request)) { 35 | return dataToJson(bookDao.getBookByIsbn(getParamIsbn(request))); 36 | } 37 | return ViewUtil.notAcceptable.handle(request, response); 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/app/book/BookDao.java: -------------------------------------------------------------------------------- 1 | package app.book; 2 | 3 | import com.google.common.collect.*; 4 | import java.util.*; 5 | 6 | public class BookDao { 7 | 8 | private final List books = ImmutableList.of( 9 | new Book("Moby Dick", "Herman Melville", "9789583001215"), 10 | new Book("A Christmas Carol", "Charles Dickens", "9780141324524"), 11 | new Book("Pride and Prejudice", "Jane Austen", "9781936594290"), 12 | new Book("The Fellowship of The Ring", "J. R. R. Tolkien", "0007171978"), 13 | new Book("Harry Potter", "J. K. Rowling", "0747532699"), 14 | new Book("War and Peace", "Leo Tolstoy", "9780060798871"), 15 | new Book("Don Quixote", "Miguel Cervantes", "9789626345221"), 16 | new Book("Ulysses", "James Joyce", "9780394703800"), 17 | new Book("The Great Gatsby", "F. Scott Fitzgerald", "9780743273565"), 18 | new Book("One Hundred Years of Solitude", "Gabriel Garcia Marquez", "9780060531041"), 19 | new Book("The adventures of Huckleberry Finn", "Mark Twain", "9781591940296"), 20 | new Book("Alice In Wonderland", "Lewis Carrol", "9780439291491") 21 | ); 22 | 23 | public Iterable getAllBooks() { 24 | return books; 25 | } 26 | 27 | public Book getBookByIsbn(String isbn) { 28 | return books.stream().filter(b -> b.getIsbn().equals(isbn)).findFirst().orElse(null); 29 | } 30 | 31 | public Book getRandomBook() { 32 | return books.get(new Random().nextInt(books.size())); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/app/index/IndexController.java: -------------------------------------------------------------------------------- 1 | package app.index; 2 | 3 | import app.util.*; 4 | import spark.*; 5 | import java.util.*; 6 | import static app.Application.*; 7 | 8 | public class IndexController { 9 | public static Route serveIndexPage = (Request request, Response response) -> { 10 | Map model = new HashMap<>(); 11 | model.put("users", userDao.getAllUserNames()); 12 | model.put("book", bookDao.getRandomBook()); 13 | return ViewUtil.render(request, model, Path.Template.INDEX); 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/app/login/LoginController.java: -------------------------------------------------------------------------------- 1 | package app.login; 2 | 3 | import app.user.*; 4 | import app.util.*; 5 | import spark.*; 6 | import java.util.*; 7 | import static app.util.RequestUtil.*; 8 | 9 | public class LoginController { 10 | 11 | public static Route serveLoginPage = (Request request, Response response) -> { 12 | Map model = new HashMap<>(); 13 | model.put("loggedOut", removeSessionAttrLoggedOut(request)); 14 | model.put("loginRedirect", removeSessionAttrLoginRedirect(request)); 15 | return ViewUtil.render(request, model, Path.Template.LOGIN); 16 | }; 17 | 18 | public static Route handleLoginPost = (Request request, Response response) -> { 19 | Map model = new HashMap<>(); 20 | if (!UserController.authenticate(getQueryUsername(request), getQueryPassword(request))) { 21 | model.put("authenticationFailed", true); 22 | return ViewUtil.render(request, model, Path.Template.LOGIN); 23 | } 24 | model.put("authenticationSucceeded", true); 25 | request.session().attribute("currentUser", getQueryUsername(request)); 26 | if (getQueryLoginRedirect(request) != null) { 27 | response.redirect(getQueryLoginRedirect(request)); 28 | } 29 | return ViewUtil.render(request, model, Path.Template.LOGIN); 30 | }; 31 | 32 | public static Route handleLogoutPost = (Request request, Response response) -> { 33 | request.session().removeAttribute("currentUser"); 34 | request.session().attribute("loggedOut", true); 35 | response.redirect(Path.Web.LOGIN); 36 | return null; 37 | }; 38 | 39 | // The origin of the request (request.pathInfo()) is saved in the session so 40 | // the user can be redirected back after login 41 | public static void ensureUserIsLoggedIn(Request request, Response response) { 42 | if (request.session().attribute("currentUser") == null) { 43 | request.session().attribute("loginRedirect", request.pathInfo()); 44 | response.redirect(Path.Web.LOGIN); 45 | } 46 | }; 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/app/user/User.java: -------------------------------------------------------------------------------- 1 | package app.user; 2 | 3 | import lombok.*; 4 | 5 | @Value // All fields are private and final. Getters (but not setters) are generated (https://projectlombok.org/features/Value.html) 6 | public class User { 7 | String username; 8 | String salt; 9 | String hashedPassword; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/app/user/UserController.java: -------------------------------------------------------------------------------- 1 | package app.user; 2 | 3 | import org.mindrot.jbcrypt.*; 4 | import static app.Application.userDao; 5 | 6 | public class UserController { 7 | 8 | // Authenticate the user by hashing the inputted password using the stored salt, 9 | // then comparing the generated hashed password to the stored hashed password 10 | public static boolean authenticate(String username, String password) { 11 | if (username.isEmpty() || password.isEmpty()) { 12 | return false; 13 | } 14 | User user = userDao.getUserByUsername(username); 15 | if (user == null) { 16 | return false; 17 | } 18 | String hashedPassword = BCrypt.hashpw(password, user.getSalt()); 19 | return hashedPassword.equals(user.getHashedPassword()); 20 | } 21 | 22 | // This method doesn't do anything, it's just included as an example 23 | public static void setPassword(String username, String oldPassword, String newPassword) { 24 | if (authenticate(username, oldPassword)) { 25 | String newSalt = BCrypt.gensalt(); 26 | String newHashedPassword = BCrypt.hashpw(newSalt, newPassword); 27 | // Update the user salt and password 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/app/user/UserDao.java: -------------------------------------------------------------------------------- 1 | package app.user; 2 | 3 | import com.google.common.collect.*; 4 | import java.util.*; 5 | import java.util.stream.*; 6 | 7 | public class UserDao { 8 | 9 | private final List users = ImmutableList.of( 10 | // Username Salt for hash Hashed password (the password is "password" for all users) 11 | new User("perwendel", "$2a$10$h.dl5J86rGH7I8bD9bZeZe", "$2a$10$h.dl5J86rGH7I8bD9bZeZeci0pDt0.VwFTGujlnEaZXPf/q7vM5wO"), 12 | new User("davidase", "$2a$10$e0MYzXyjpJS7Pd0RVvHwHe", "$2a$10$e0MYzXyjpJS7Pd0RVvHwHe1HlCS4bZJ18JuywdEMLT83E1KDmUhCy"), 13 | new User("federico", "$2a$10$E3DgchtVry3qlYlzJCsyxe", "$2a$10$E3DgchtVry3qlYlzJCsyxeSK0fftK4v0ynetVCuDdxGVl1obL.ln2") 14 | ); 15 | 16 | public User getUserByUsername(String username) { 17 | return users.stream().filter(b -> b.getUsername().equals(username)).findFirst().orElse(null); 18 | } 19 | 20 | public Iterable getAllUserNames() { 21 | return users.stream().map(User::getUsername).collect(Collectors.toList()); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/app/util/Filters.java: -------------------------------------------------------------------------------- 1 | package app.util; 2 | 3 | import spark.*; 4 | import static app.util.RequestUtil.*; 5 | 6 | public class Filters { 7 | 8 | // If a user manually manipulates paths and forgets to add 9 | // a trailing slash, redirect the user to the correct path 10 | public static Filter addTrailingSlashes = (Request request, Response response) -> { 11 | if (!request.pathInfo().endsWith("/")) { 12 | response.redirect(request.pathInfo() + "/"); 13 | } 14 | }; 15 | 16 | // Locale change can be initiated from any page 17 | // The locale is extracted from the request and saved to the user's session 18 | public static Filter handleLocaleChange = (Request request, Response response) -> { 19 | if (getQueryLocale(request) != null) { 20 | request.session().attribute("locale", getQueryLocale(request)); 21 | response.redirect(request.pathInfo()); 22 | } 23 | }; 24 | 25 | // Enable GZIP for all responses 26 | public static Filter addGzipHeader = (Request request, Response response) -> { 27 | response.header("Content-Encoding", "gzip"); 28 | }; 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/app/util/JsonUtil.java: -------------------------------------------------------------------------------- 1 | package app.util; 2 | 3 | import com.fasterxml.jackson.databind.*; 4 | import java.io.*; 5 | 6 | public class JsonUtil { 7 | public static String dataToJson(Object data) { 8 | try { 9 | ObjectMapper mapper = new ObjectMapper(); 10 | mapper.enable(SerializationFeature.INDENT_OUTPUT); 11 | StringWriter sw = new StringWriter(); 12 | mapper.writeValue(sw, data); 13 | return sw.toString(); 14 | } catch (IOException e) { 15 | throw new RuntimeException("IOEXception while mapping object (" + data + ") to JSON"); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/app/util/MessageBundle.java: -------------------------------------------------------------------------------- 1 | package app.util; 2 | 3 | import java.text.*; 4 | import java.util.*; 5 | 6 | public class MessageBundle { 7 | 8 | private ResourceBundle messages; 9 | 10 | public MessageBundle(String languageTag) { 11 | Locale locale = languageTag != null ? new Locale(languageTag) : Locale.ENGLISH; 12 | this.messages = ResourceBundle.getBundle("localization/messages", locale); 13 | } 14 | 15 | public String get(String message) { 16 | return messages.getString(message); 17 | } 18 | 19 | public final String get(final String key, final Object... args) { 20 | return MessageFormat.format(get(key), args); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/app/util/Path.java: -------------------------------------------------------------------------------- 1 | package app.util; 2 | 3 | import lombok.*; 4 | 5 | public class Path { 6 | 7 | // The @Getter methods are needed in order to access 8 | // the variables from Velocity Templates 9 | public static class Web { 10 | @Getter public static final String INDEX = "/index/"; 11 | @Getter public static final String LOGIN = "/login/"; 12 | @Getter public static final String LOGOUT = "/logout/"; 13 | @Getter public static final String BOOKS = "/books/"; 14 | @Getter public static final String ONE_BOOK = "/books/:isbn/"; 15 | } 16 | 17 | public static class Template { 18 | public final static String INDEX = "/velocity/index/index.vm"; 19 | public final static String LOGIN = "/velocity/login/login.vm"; 20 | public final static String BOOKS_ALL = "/velocity/book/all.vm"; 21 | public static final String BOOKS_ONE = "/velocity/book/one.vm"; 22 | public static final String NOT_FOUND = "/velocity/notFound.vm"; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/app/util/RequestUtil.java: -------------------------------------------------------------------------------- 1 | package app.util; 2 | 3 | import spark.*; 4 | 5 | public class RequestUtil { 6 | 7 | public static String getQueryLocale(Request request) { 8 | return request.queryParams("locale"); 9 | } 10 | 11 | public static String getParamIsbn(Request request) { 12 | return request.params("isbn"); 13 | } 14 | 15 | public static String getQueryUsername(Request request) { 16 | return request.queryParams("username"); 17 | } 18 | 19 | public static String getQueryPassword(Request request) { 20 | return request.queryParams("password"); 21 | } 22 | 23 | public static String getQueryLoginRedirect(Request request) { 24 | return request.queryParams("loginRedirect"); 25 | } 26 | 27 | public static String getSessionLocale(Request request) { 28 | return request.session().attribute("locale"); 29 | } 30 | 31 | public static String getSessionCurrentUser(Request request) { 32 | return request.session().attribute("currentUser"); 33 | } 34 | 35 | public static boolean removeSessionAttrLoggedOut(Request request) { 36 | Object loggedOut = request.session().attribute("loggedOut"); 37 | request.session().removeAttribute("loggedOut"); 38 | return loggedOut != null; 39 | } 40 | 41 | public static String removeSessionAttrLoginRedirect(Request request) { 42 | String loginRedirect = request.session().attribute("loginRedirect"); 43 | request.session().removeAttribute("loginRedirect"); 44 | return loginRedirect; 45 | } 46 | 47 | public static boolean clientAcceptsHtml(Request request) { 48 | String accept = request.headers("Accept"); 49 | return accept != null && accept.contains("text/html"); 50 | } 51 | 52 | public static boolean clientAcceptsJson(Request request) { 53 | String accept = request.headers("Accept"); 54 | return accept != null && accept.contains("application/json"); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/app/util/ViewUtil.java: -------------------------------------------------------------------------------- 1 | package app.util; 2 | 3 | import org.apache.velocity.app.*; 4 | import org.eclipse.jetty.http.*; 5 | import spark.*; 6 | import spark.template.velocity.*; 7 | import java.util.*; 8 | import static app.util.RequestUtil.*; 9 | 10 | public class ViewUtil { 11 | 12 | // Renders a template given a model and a request 13 | // The request is needed to check the user session for language settings 14 | // and to see if the user is logged in 15 | public static String render(Request request, Map model, String templatePath) { 16 | model.put("msg", new MessageBundle(getSessionLocale(request))); 17 | model.put("currentUser", getSessionCurrentUser(request)); 18 | model.put("WebPath", Path.Web.class); // Access application URLs from templates 19 | return strictVelocityEngine().render(new ModelAndView(model, templatePath)); 20 | } 21 | 22 | public static Route notAcceptable = (Request request, Response response) -> { 23 | response.status(HttpStatus.NOT_ACCEPTABLE_406); 24 | return new MessageBundle(getSessionLocale(request)).get("ERROR_406_NOT_ACCEPTABLE"); 25 | }; 26 | 27 | public static Route notFound = (Request request, Response response) -> { 28 | response.status(HttpStatus.NOT_FOUND_404); 29 | return render(request, new HashMap<>(), Path.Template.NOT_FOUND); 30 | }; 31 | 32 | private static VelocityTemplateEngine strictVelocityEngine() { 33 | VelocityEngine configuredEngine = new VelocityEngine(); 34 | configuredEngine.setProperty("runtime.references.strict", true); 35 | configuredEngine.setProperty("resource.loader", "class"); 36 | configuredEngine.setProperty("class.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader"); 37 | return new VelocityTemplateEngine(configuredEngine); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/resources/localization/messages_de.properties: -------------------------------------------------------------------------------- 1 | ## Common 2 | COMMON_TITLE=Ze Spark Library 3 | COMMON_FOOTER_TEXT=Ze application uses OpenLibrary vor images. 4 | COMMON_NAV_ALLBOOKS=View all ze books 5 | COMMON_NAV_LOGIN=Innlogg 6 | COMMON_NAV_LOGOUT=Auslogg 7 | ERROR_406_NOT_ACCEPTABLE=No zuitable content found. Please zpecify eizer 'html/text' or 'application/json'. 8 | ERROR_404_NOT_FOUND=Ve cannot find ze page you are looking for (error 404) 9 | 10 | 11 | ## Index 12 | INDEX_HEADING=Velcome to ze Spark Library 13 | INDEX_REGISTERED_USERS=Zere are currently {0} users registered: 14 | INDEX_PASSWORD_INFO=It seems zey have all chosen ze password "password" for some reason. Hov silly. 15 | INDEX_BOOK_OF_THE_DAY_TEXT=Ze book of ze day is: 16 | INDEX_BOOK_OF_THE_DAY_LINK={0} von {1} 17 | 18 | 19 | ## Login 20 | LOGIN_HEADING=Innlogg 21 | LOGIN_INSTRUCTIONS=Please enter dein username und password.
(Zee ze index page if you need a hint)
22 | LOGIN_AUTH_SUCCEEDED=You''re logged in as ''{0}''. 23 | LOGIN_AUTH_FAILED=Ze login informazion you zuplied vas incorrect. 24 | LOGIN_LOGGED_OUT=You have been logged aus. 25 | LOGIN_LABEL_USERNAME=Username 26 | LOGIN_LABEL_PASSWORD=Password 27 | LOGIN_BUTTON_LOGIN=Innlogg 28 | 29 | 30 | ## Books 31 | BOOKS_HEADING_ALL=All ze books 32 | BOOKS_CAPTION={0}
von {1} 33 | BOOKS_BOOK_NOT_FOUND=Book nicht found 34 | -------------------------------------------------------------------------------- /src/main/resources/localization/messages_en.properties: -------------------------------------------------------------------------------- 1 | ## Common 2 | COMMON_TITLE=Spark Library 3 | COMMON_FOOTER_TEXT=This Application uses OpenLibrary for images. 4 | COMMON_NAV_ALLBOOKS=View all books 5 | COMMON_NAV_LOGIN=Log in 6 | COMMON_NAV_LOGOUT=Log out 7 | ERROR_406_NOT_ACCEPTABLE=No suitable content found. Please specify either 'html/text' or 'application/json'. 8 | ERROR_404_NOT_FOUND=We can't find the page you're looking for (error 404) 9 | 10 | 11 | ## Index 12 | INDEX_HEADING=Welcome to the Spark Library 13 | INDEX_REGISTERED_USERS=There are currently {0} users registered: 14 | INDEX_PASSWORD_INFO=It seems they've all chosen the password "password" for some reason. How silly. 15 | INDEX_BOOK_OF_THE_DAY_TEXT=The book of the day is: 16 | INDEX_BOOK_OF_THE_DAY_LINK={0} by {1} 17 | 18 | 19 | ## Login 20 | LOGIN_HEADING=Login 21 | LOGIN_INSTRUCTIONS=Please enter your username and password.
(See the index page if you need a hint)
22 | LOGIN_AUTH_SUCCEEDED=You''re logged in as ''{0}''. 23 | LOGIN_AUTH_FAILED=The login information you supplied was incorrect. 24 | LOGIN_LOGGED_OUT=You have been logged out. 25 | LOGIN_LABEL_USERNAME=Username 26 | LOGIN_LABEL_PASSWORD=Password 27 | LOGIN_BUTTON_LOGIN=Log in 28 | 29 | 30 | ## Books 31 | BOOKS_HEADING_ALL=All books 32 | BOOKS_CAPTION={0}
by {1} 33 | BOOKS_BOOK_NOT_FOUND=Book not found 34 | -------------------------------------------------------------------------------- /src/main/resources/public/img/english.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tipsy/spark-basic-structure/5af6b78e0869dd2b2f014274ef731a2468ed0b8e/src/main/resources/public/img/english.png -------------------------------------------------------------------------------- /src/main/resources/public/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tipsy/spark-basic-structure/5af6b78e0869dd2b2f014274ef731a2468ed0b8e/src/main/resources/public/img/favicon.png -------------------------------------------------------------------------------- /src/main/resources/public/img/german.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tipsy/spark-basic-structure/5af6b78e0869dd2b2f014274ef731a2468ed0b8e/src/main/resources/public/img/german.png -------------------------------------------------------------------------------- /src/main/resources/public/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tipsy/spark-basic-structure/5af6b78e0869dd2b2f014274ef731a2468ed0b8e/src/main/resources/public/img/logo.png -------------------------------------------------------------------------------- /src/main/resources/public/main.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html, body { 6 | margin: 0; 7 | padding: 0; 8 | font-family: Tahoma, Arial, sans-serif; 9 | position: relative; 10 | min-height: 100%; 11 | } 12 | 13 | body { 14 | padding: 0 0 40px; 15 | color: #333; 16 | background: #f9f9f9; 17 | } 18 | 19 | h1, h2, h3, h4 { 20 | font-family: monospace; 21 | font-weight: 300; 22 | color: #444; 23 | } 24 | 25 | small { 26 | color: #555; 27 | } 28 | 29 | header { 30 | background: #274555; 31 | border-bottom: 5px solid #ff7761; 32 | box-shadow: 0 1px 0 0 rgba(0,0,0,.6); 33 | } 34 | 35 | nav { 36 | padding: 15px; 37 | margin: 0 auto; 38 | max-width: 800px; 39 | position: relative; 40 | } 41 | 42 | nav #menu { 43 | margin-top: 20px; 44 | float: right; 45 | } 46 | 47 | a { 48 | text-decoration: none; 49 | color: #ff7761; 50 | } 51 | 52 | #menu li { 53 | float: left; 54 | margin: 0 10px; 55 | } 56 | 57 | #menu li a, #logout { 58 | background: transparent; 59 | cursor: pointer; 60 | border: 0; 61 | font-size: 16px; 62 | display: inline-block; 63 | color: #fff; 64 | text-align: center; 65 | height: 30px; 66 | line-height: 30px; 67 | padding: 0 10px; 68 | text-decoration: none; 69 | } 70 | 71 | #logo { 72 | max-height: 50px; 73 | } 74 | 75 | #chooseLanguage { 76 | top: 15px; 77 | right: 35px; 78 | position: absolute; 79 | } 80 | 81 | #chooseLanguage li { 82 | float: left; 83 | } 84 | 85 | #chooseLanguage button { 86 | cursor: pointer; 87 | margin-left: 8px; 88 | width: 18px; 89 | height: 18px; 90 | border-radius: 9px; 91 | opacity: 0.6; 92 | border: 1px solid #222; 93 | background-size: 100%; 94 | } 95 | 96 | #chooseLanguage button:hover { 97 | opacity: 0.8; 98 | } 99 | 100 | main { 101 | max-width: 800px; 102 | margin: 0 auto; 103 | padding: 15px; 104 | } 105 | 106 | #content { 107 | padding: 15px; 108 | background: #fff; 109 | box-shadow: 0 1px 5px rgba(0, 0, 0, 0.25); 110 | } 111 | 112 | footer { 113 | position: absolute; 114 | left: 0; 115 | bottom: 0; 116 | height: 40px; 117 | line-height: 40px; 118 | width: 100%; 119 | text-align: center; 120 | background: #fff; 121 | border-top: 1px solid #f9a11b; 122 | font-size: 14px; 123 | } 124 | 125 | nav ul, nav li { 126 | margin: 0; 127 | padding: 0; 128 | list-style-type: none; 129 | } 130 | 131 | /* Needlessly fancy menu hover effect */ 132 | 133 | #menu li { 134 | position: relative; 135 | } 136 | 137 | #menu li a::after, #logout:after { 138 | position: absolute; 139 | top: 28px; 140 | left: 0; 141 | width: 100%; 142 | height: 4px; 143 | background: rgba(255, 255, 255, 0.5); 144 | border-radius: 5px; 145 | content: ''; 146 | opacity: 0; 147 | -webkit-transition: opacity 0.3s, -webkit-transform 0.3s; 148 | transition: opacity 0.3s, transform 0.3s; 149 | -webkit-transform: translateY(10px); 150 | transform: translateY(10px); 151 | } 152 | 153 | #logout:hover::after, 154 | #logout:focus::after, 155 | #menu li a:hover::after, 156 | #menu li a:focus::after { 157 | opacity: 1; 158 | -webkit-transform: translateY(0px); 159 | transform: translateY(0px); 160 | } 161 | 162 | /* Very basic grid */ 163 | .row { 164 | width: 100%; 165 | overflow: auto; 166 | } 167 | 168 | .row > * { 169 | float: left; 170 | } 171 | 172 | .row-2 .col { 173 | width: 49% 174 | } 175 | 176 | .row-2 .col:nth-child(odd) { 177 | margin: 0% 1% 0% 0% 178 | } 179 | 180 | .row-2 .col:nth-child(even) { 181 | margin: 0% 0% 0% 1% 182 | } 183 | 184 | .row-3 .col { 185 | width: 32% 186 | } 187 | 188 | .row-3 .col:nth-child(3n+1) { 189 | margin: 0% 1% 0% 0% 190 | } 191 | 192 | .row-3 .col:nth-child(3n+2) { 193 | margin: 0% 1% 0% 1% 194 | } 195 | 196 | .row-3 .col:nth-child(3n+3) { 197 | margin: 0% 0% 0% 1% 198 | } 199 | 200 | @media screen and (max-width: 550px) { 201 | .row .col:nth-child(n) { 202 | width: 100%; 203 | margin-right: 0; 204 | margin-left: 0; 205 | } 206 | } 207 | 208 | .col img { 209 | display: block; 210 | width: 100%; 211 | } 212 | 213 | /* Book related stuff */ 214 | 215 | a.book { 216 | display: block; 217 | text-align: center; 218 | text-decoration: none; 219 | color: #333; 220 | padding: 10px; 221 | border-radius: 5px; 222 | } 223 | 224 | a.book:hover { 225 | background: #e2e9f5; 226 | } 227 | 228 | a .bookCover { 229 | padding: 10px; 230 | display: flex; 231 | align-items: center; 232 | justify-content: center; 233 | } 234 | 235 | a .bookCover img { 236 | border-radius: 5px; 237 | min-height: 200px; 238 | max-height: 200px; 239 | width: auto; 240 | } 241 | 242 | .bookCover img { 243 | margin-top: 20px; 244 | border-radius: 10px; 245 | width: 100%; 246 | } 247 | 248 | /* Login Form */ 249 | 250 | #loginForm { 251 | max-width: 400px; 252 | margin: 0 auto; 253 | } 254 | 255 | #loginForm label { 256 | display: block; 257 | width: 100% 258 | } 259 | 260 | #loginForm input { 261 | border: 1px solid #ddd; 262 | padding: 8px 12px; 263 | width: 100%; 264 | border-radius: 3px; 265 | margin: 2px 0 20px 0; 266 | } 267 | 268 | #loginForm input[type="submit"] { 269 | color: white; 270 | background: #274555; 271 | border: 0; 272 | cursor: pointer; 273 | } 274 | 275 | .notification { 276 | padding: 10px; 277 | background: #333; 278 | color: white; 279 | border-radius: 3px; 280 | } 281 | 282 | .good.notification { 283 | background: #008900; 284 | } 285 | 286 | .bad.notification { 287 | background: #bb0000; 288 | } 289 | -------------------------------------------------------------------------------- /src/main/resources/velocity/book/all.vm: -------------------------------------------------------------------------------- 1 | #parse("/velocity/layout.vm") 2 | #@mainLayout() 3 |

$msg.get("BOOKS_HEADING_ALL")

4 |
5 | #foreach($book in $books) 6 | 14 | #end 15 |
16 | #end 17 | -------------------------------------------------------------------------------- /src/main/resources/velocity/book/one.vm: -------------------------------------------------------------------------------- 1 | #parse("/velocity/layout.vm") 2 | #@mainLayout() 3 | #if($book) 4 |

$book.getTitle()

5 |

$book.getAuthor()

6 |
7 |
8 | $book.getTitle() 9 |
10 |
11 | #else 12 |

$msg.get("BOOKS_BOOK_NOT_FOUND")

13 | #end 14 | #end 15 | -------------------------------------------------------------------------------- /src/main/resources/velocity/index/index.vm: -------------------------------------------------------------------------------- 1 | #parse("/velocity/layout.vm") 2 | #@mainLayout() 3 |

$msg.get("INDEX_HEADING")

4 |

$msg.get("INDEX_REGISTERED_USERS", $users.size())

5 |
    6 | #foreach($user in $users) 7 |
  • $user
  • 8 | #end 9 |
10 |

$msg.get("INDEX_PASSWORD_INFO")

11 | #if($book) 12 |

$msg.get("INDEX_BOOK_OF_THE_DAY_TEXT")

13 | 21 | #end 22 | #end 23 | -------------------------------------------------------------------------------- /src/main/resources/velocity/layout.vm: -------------------------------------------------------------------------------- 1 | #macro(mainLayout) 2 | 3 | 4 | $msg.get("COMMON_TITLE") 5 | 6 | 7 | 8 | 9 | 10 |
11 | 36 |
37 |
38 |
39 | $bodyContent 40 |
41 |
42 |
43 | $msg.get("COMMON_FOOTER_TEXT") 44 |
45 | 46 | 47 | #end 48 | -------------------------------------------------------------------------------- /src/main/resources/velocity/login/login.vm: -------------------------------------------------------------------------------- 1 | #parse("/velocity/layout.vm") 2 | #@mainLayout() 3 |
4 | #if($authenticationFailed) 5 |

$msg.get("LOGIN_AUTH_FAILED")

6 | #elseif($authenticationSucceeded) 7 |

$msg.get("LOGIN_AUTH_SUCCEEDED", $currentUser)

8 | #elseif($loggedOut) 9 |

$msg.get("LOGIN_LOGGED_OUT")

10 | #end 11 |

$msg.get("LOGIN_HEADING")

12 |

$msg.get("LOGIN_INSTRUCTIONS", $WebPath.getINDEX())

13 | 14 | 15 | 16 | 17 | #if($loginRedirect) 18 | 19 | #end 20 | 21 |
22 | #end 23 | -------------------------------------------------------------------------------- /src/main/resources/velocity/notFound.vm: -------------------------------------------------------------------------------- 1 | #parse("/velocity/layout.vm") 2 | #@mainLayout() 3 |

$msg.get("ERROR_404_NOT_FOUND")

4 | #end 5 | -------------------------------------------------------------------------------- /src/main/resources/velocityconfig/velocity_implicit.vm: -------------------------------------------------------------------------------- 1 | #* @implicitly included *# 2 | #* @vtlvariable name="WebPath" type="app.util.Path.Web" *# 3 | #* @vtlvariable name="msg" type="app.util.MessageBundle" *# 4 | #* @vtlvariable name="books" type="java.lang.Iterable" *# 5 | #* @vtlvariable name="book" type="app.book.Book" *# 6 | #* @vtlvariable name="users" type="java.lang.Iterable" *# 7 | #* @vtlvariable name="currentUser" type="java.lang.String" *# 8 | #* @vtlvariable name="loggedOut" type="java.lang.String" *# 9 | #* @vtlvariable name="authenticationFailed" type="java.lang.String" *# 10 | #* @vtlvariable name="authenticationSucceeded" type="java.lang.String" *# 11 | #* @vtlvariable name="loginRedirect" type="java.lang.String" *# 12 | --------------------------------------------------------------------------------