├── .github ├── dependabot.yml └── workflows │ ├── auto-merge.yml │ └── workflow.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── doc ├── rate-my-cat-screeshot-1.png └── rate-my-cat-screeshot-2.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── pom.xml └── src ├── main ├── java │ └── io │ │ └── github │ │ └── bonigarcia │ │ ├── Cat.java │ │ ├── CatException.java │ │ ├── CatRepository.java │ │ ├── CatService.java │ │ ├── CookiesService.java │ │ ├── DatabasePopulator.java │ │ ├── Opinion.java │ │ ├── RateMyCatWebApp.java │ │ └── WebController.java └── resources │ ├── application.properties │ ├── logback.xml │ ├── static │ ├── css │ │ ├── carousel.css │ │ ├── gallery-grid.css │ │ └── star-rating.css │ ├── img │ │ ├── baby.jpg │ │ ├── bella.jpg │ │ ├── gizmo.jpg │ │ ├── kitty.jpg │ │ ├── luna.jpg │ │ ├── rate-my-cat.png │ │ ├── shadow.jpg │ │ ├── smokey.jpg │ │ ├── stars-0.0.png │ │ ├── stars-0.5.png │ │ ├── stars-1.0.png │ │ ├── stars-1.5.png │ │ ├── stars-2.0.png │ │ ├── stars-2.5.png │ │ ├── stars-3.0.png │ │ ├── stars-3.5.png │ │ ├── stars-4.0.png │ │ ├── stars-4.5.png │ │ ├── stars-5.0.png │ │ ├── tigger.jpg │ │ └── toby.jpg │ └── js │ │ └── star-rating.js │ └── templates │ └── index.html └── test └── java └── io └── github └── bonigarcia └── test ├── e2e └── UserInferfaceTest.java ├── integration └── WebContextTest.java └── unit ├── CookiesTest.java └── RateCatsTest.java /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: maven 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: '06:00' 8 | open-pull-requests-limit: 99 9 | - package-ecosystem: gradle 10 | directory: "/" 11 | schedule: 12 | interval: daily 13 | time: '06:00' 14 | open-pull-requests-limit: 99 15 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: pull_request 3 | 4 | permissions: 5 | contents: write 6 | pull-requests: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: ${{ github.actor == 'dependabot[bot]' }} 12 | steps: 13 | - name: Dependabot metadata 14 | id: metadata 15 | uses: dependabot/fetch-metadata@v1.1.1 16 | with: 17 | github-token: "${{ secrets.GITHUB_TOKEN }}" 18 | - name: Enable auto-merge for Dependabot PRs 19 | run: | 20 | gh pr merge "$PR_URL" --auto --squash --body="Co-authored-by: Boni Garcia " 21 | env: 22 | PR_URL: ${{github.event.pull_request.html_url}} 23 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 24 | -------------------------------------------------------------------------------- /.github/workflows/workflow.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | DISPLAY: :99 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout GitHub repo 17 | uses: actions/checkout@v2 18 | - name: Set up Java 19 | uses: actions/setup-java@v1 20 | with: 21 | java-version: 17 22 | - name: Start Xvfb 23 | run: Xvfb :99 & 24 | - name: Test with Maven 25 | run: mvn -B test 26 | - name: Test with Gradle 27 | run: ./gradlew test 28 | - name: Upload analysis to SonarCloud 29 | if: success() && !contains(github.ref, 'pull') 30 | run: > 31 | mvn -B sonar:sonar 32 | -Dsonar.host.url=https://sonarcloud.io 33 | -Dsonar.organization=bonigarcia-github 34 | -Dsonar.projectKey=io.github.bonigarcia:rate-my-cat 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 37 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 38 | - name: Upload coverage to Codecov 39 | uses: codecov/codecov-action@v1 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | target 3 | .classpath 4 | .project 5 | .settings 6 | .metadata 7 | .gradle 8 | .idea 9 | .allure 10 | *.iml 11 | build 12 | bin 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rate my cat! [![][Logo]][GitHub Repository] 2 | 3 | [![Build Status](https://github.com/bonigarcia/rate-my-cat/workflows/build/badge.svg)](https://github.com/bonigarcia/rate-my-cat/actions) 4 | [![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=io.github.bonigarcia:rate-my-cat&metric=alert_status)](https://sonarcloud.io/project/overview?id=io.github.bonigarcia%3Arate-my-cat) 5 | [![codecov](https://codecov.io/gh/bonigarcia/rate-my-cat/branch/master/graph/badge.svg)](https://codecov.io/gh/bonigarcia/rate-my-cat) 6 | 7 | This project contains a complete sample application for the book [Mastering Software Testing with JUnit 5]. It consists on a web application in which end uses can rate a list of cats by watching its name and picture. The rate shall be done once per end user using a star mechanism. Optionally, comments can be made per cat. This application has been built using the following technologies: 8 | 9 | * Spring Framework, as application framework: Spring Boot, Spring MVC + Thymeleaf, Spring Data JPA, and Spring Test (for integration tests). 10 | * JUnit 5, as testing framework. 11 | * Hamcrest, for improving the readability of assertions. 12 | * Mockito, for unit testing. 13 | * Selenium WebDriver, for end-to-end testing. 14 | 15 | The screenshots below show the application GUI in action. 16 | 17 | ![][Screeshot 1] 18 | ![][Screeshot 2] 19 | 20 | # About 21 | 22 | This is a project made by [Boni Garcia], Associate Professor at [Universidad Carlos III de Madrid], Spain. Copyright © 2017-2023. 23 | 24 | [Boni Garcia]: https://bonigarcia.github.io/ 25 | [Universidad Carlos III de Madrid]: https://www.it.uc3m.es/bogarcia/index.html 26 | [GitHub Repository]: https://github.com/bonigarcia/rate-my-cat 27 | [Logo]: https://raw.githubusercontent.com/bonigarcia/rate-my-cat/master/src/main/resources/static/img/rate-my-cat.png 28 | [Screeshot 1]: https://raw.githubusercontent.com/bonigarcia/rate-my-cat/master/doc/rate-my-cat-screeshot-1.png 29 | [Screeshot 2]: https://raw.githubusercontent.com/bonigarcia/rate-my-cat/master/doc/rate-my-cat-screeshot-2.png 30 | [Mastering Software Testing with JUnit 5]: https://www.amazon.com/Mastering-Software-Testing-JUnit-Comprehensive-ebook/dp/B076ZQCK5Q 31 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | springBootVersion = '3.5.0' 4 | } 5 | repositories { 6 | mavenCentral() 7 | } 8 | dependencies { 9 | classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 10 | } 11 | } 12 | 13 | repositories { 14 | mavenCentral() 15 | } 16 | 17 | ext { 18 | seleniumVersion = '4.33.0' 19 | seleniumJupiterVersion = '6.1.1' 20 | httpclient5Version = '5.4.3' 21 | } 22 | 23 | apply plugin: 'java' 24 | apply plugin: 'eclipse' 25 | apply plugin: 'idea' 26 | apply plugin: 'org.springframework.boot' 27 | apply plugin: 'io.spring.dependency-management' 28 | 29 | jar { 30 | baseName = 'rate-my-cat' 31 | version = '1.0.0' 32 | } 33 | 34 | test { 35 | useJUnitPlatform() 36 | 37 | testLogging { 38 | events "passed", "skipped", "failed" 39 | showStandardStreams = true 40 | } 41 | } 42 | 43 | compileTestJava { 44 | sourceCompatibility = 17 45 | targetCompatibility = 17 46 | options.compilerArgs += '-parameters' 47 | } 48 | 49 | dependencies { 50 | implementation("org.springframework.boot:spring-boot-starter-web") 51 | implementation("org.springframework.boot:spring-boot-starter-thymeleaf") 52 | implementation("org.springframework.boot:spring-boot-starter-data-jpa") 53 | implementation("com.h2database:h2") 54 | 55 | testImplementation("org.springframework.boot:spring-boot-starter-test") 56 | testImplementation("org.hamcrest:hamcrest-core") 57 | testImplementation("org.mockito:mockito-junit-jupiter") 58 | testImplementation("org.junit.jupiter:junit-jupiter") 59 | testImplementation("org.seleniumhq.selenium:selenium-java:${seleniumVersion}") 60 | testImplementation("io.github.bonigarcia:selenium-jupiter:${seleniumJupiterVersion}") 61 | testImplementation("org.apache.httpcomponents.client5:httpclient5:${httpclient5Version}") 62 | } 63 | -------------------------------------------------------------------------------- /doc/rate-my-cat-screeshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bonigarcia/rate-my-cat/c244829e2ff07967fd6d1a5484c7e5bcefc48f7a/doc/rate-my-cat-screeshot-1.png -------------------------------------------------------------------------------- /doc/rate-my-cat-screeshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bonigarcia/rate-my-cat/c244829e2ff07967fd6d1a5484c7e5bcefc48f7a/doc/rate-my-cat-screeshot-2.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bonigarcia/rate-my-cat/c244829e2ff07967fd6d1a5484c7e5bcefc48f7a/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | io.github.bonigarcia 6 | rate-my-cat 7 | 1.0.0 8 | 9 | 10 | org.springframework.boot 11 | spring-boot-starter-parent 12 | 3.4.4 13 | 14 | 15 | 16 | 4.33.0 17 | 6.1.1 18 | 5.5 19 | 20 | 3.0.0-M6 21 | 0.8.13 22 | 23 | 17 24 | ${java.version} 25 | ${java.version} 26 | 27 | UTF-8 28 | ${project.encondig} 29 | ${project.encondig} 30 | 31 | 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-starter-web 36 | 37 | 38 | org.springframework.boot 39 | spring-boot-starter-thymeleaf 40 | 41 | 42 | org.springframework.boot 43 | spring-boot-starter-data-jpa 44 | 45 | 46 | com.h2database 47 | h2 48 | 49 | 50 | 51 | org.springframework.boot 52 | spring-boot-starter-test 53 | test 54 | 55 | 56 | org.junit.jupiter 57 | junit-jupiter 58 | test 59 | 60 | 61 | org.hamcrest 62 | hamcrest-core 63 | test 64 | 65 | 66 | org.mockito 67 | mockito-junit-jupiter 68 | test 69 | 70 | 71 | org.seleniumhq.selenium 72 | selenium-java 73 | ${selenium.version} 74 | test 75 | 76 | 77 | io.github.bonigarcia 78 | selenium-jupiter 79 | ${selenium-jupiter.version} 80 | test 81 | 82 | 83 | org.apache.httpcomponents.client5 84 | httpclient5 85 | ${httpclient5.version} 86 | test 87 | 88 | 89 | 90 | 91 | 92 | 93 | org.apache.maven.plugins 94 | maven-surefire-plugin 95 | 96 | 97 | org.springframework.boot 98 | spring-boot-maven-plugin 99 | 100 | 101 | org.jacoco 102 | jacoco-maven-plugin 103 | ${jacoco-maven-plugin.version} 104 | 105 | 106 | 107 | prepare-agent 108 | 109 | 110 | 111 | report 112 | test 113 | 114 | report 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /src/main/java/io/github/bonigarcia/Cat.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2017 Boni Garcia (https://bonigarcia.github.io/) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package io.github.bonigarcia; 18 | 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | import java.util.Locale; 22 | import java.util.Optional; 23 | 24 | import jakarta.persistence.CascadeType; 25 | import jakarta.persistence.ElementCollection; 26 | import jakarta.persistence.Entity; 27 | import jakarta.persistence.FetchType; 28 | import jakarta.persistence.GeneratedValue; 29 | import jakarta.persistence.GenerationType; 30 | import jakarta.persistence.Id; 31 | import jakarta.persistence.OneToMany; 32 | import jakarta.persistence.Transient; 33 | 34 | @Entity 35 | public class Cat { 36 | 37 | @Id 38 | @GeneratedValue(strategy = GenerationType.AUTO) 39 | private long id; 40 | 41 | private String name; 42 | private String pictureFileName; 43 | 44 | @Transient 45 | private boolean inCookies; 46 | 47 | @ElementCollection(fetch = FetchType.EAGER) 48 | @OneToMany(cascade = CascadeType.ALL) 49 | private List opinions = new ArrayList<>(); 50 | 51 | protected Cat() { 52 | } 53 | 54 | public Cat(String name, String pictureFileName) { 55 | this.name = name; 56 | this.pictureFileName = pictureFileName; 57 | } 58 | 59 | public void rate(double stars, String comment) { 60 | opinions.add(new Opinion(stars, comment)); 61 | } 62 | 63 | public String getAverageRateAsString() { 64 | double averageRate = getAverageRate(); 65 | return averageRate > 0 ? String.format(Locale.US, "%.2f", averageRate) 66 | : "Not rated"; 67 | } 68 | 69 | public double getAverageRate() { 70 | double totalStars = 0; 71 | double average = 0; 72 | for (Opinion opinion : opinions) { 73 | totalStars += opinion.getStars(); 74 | } 75 | if (!opinions.isEmpty()) { 76 | average = totalStars / opinions.size(); 77 | } 78 | return average; 79 | } 80 | 81 | public double getHalfRoundedAverageRate() { 82 | return Math.round(getAverageRate() / 0.5) * 0.5; 83 | } 84 | 85 | public String getName() { 86 | return name; 87 | } 88 | 89 | public String getPictureFileName() { 90 | return pictureFileName; 91 | } 92 | 93 | public long getId() { 94 | return id; 95 | } 96 | 97 | public List getOpinions() { 98 | return opinions; 99 | } 100 | 101 | public int getOpinionsSize() { 102 | int opinionsSize = opinions.size(); 103 | if (isInCookies()) { 104 | opinionsSize--; 105 | } 106 | return opinionsSize; 107 | } 108 | 109 | public double getStarsInCookies() { 110 | Optional opinionsInCookies = getOpinionsInCookies(); 111 | if (opinionsInCookies.isPresent()) { 112 | return opinionsInCookies.get().getHalfRoundedStars(); 113 | } 114 | return 0; 115 | } 116 | 117 | public String getCommentInCookies() { 118 | Optional opinionsInCookies = getOpinionsInCookies(); 119 | if (opinionsInCookies.isPresent()) { 120 | return opinionsInCookies.get().getComment(); 121 | } 122 | return ""; 123 | } 124 | 125 | public Optional getOpinionsInCookies() { 126 | for (Opinion opinion : opinions) { 127 | if (opinion.isInCookies()) { 128 | return Optional.of(opinion); 129 | } 130 | } 131 | return Optional.empty(); 132 | } 133 | 134 | public void setOpinions(List opinions) { 135 | this.opinions = opinions; 136 | } 137 | 138 | public boolean isInCookies() { 139 | return inCookies; 140 | } 141 | 142 | public void setInCookies(boolean inCookies) { 143 | this.inCookies = inCookies; 144 | } 145 | 146 | @Override 147 | public String toString() { 148 | return "Cat [id=" + id + ", name=" + name + ", pictureFileName=" 149 | + pictureFileName + ", inCookies=" + inCookies + ", opinions=" 150 | + opinions + "]"; 151 | } 152 | 153 | } 154 | -------------------------------------------------------------------------------- /src/main/java/io/github/bonigarcia/CatException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2017 Boni Garcia (https://bonigarcia.github.io/) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package io.github.bonigarcia; 18 | 19 | public class CatException extends RuntimeException { 20 | 21 | private static final long serialVersionUID = 1L; 22 | 23 | public CatException(String cause) { 24 | super(cause); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/io/github/bonigarcia/CatRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2017 Boni Garcia (https://bonigarcia.github.io/) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package io.github.bonigarcia; 18 | 19 | import org.springframework.data.jpa.repository.JpaRepository; 20 | 21 | public interface CatRepository extends JpaRepository { 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/io/github/bonigarcia/CatService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2017 Boni Garcia (https://bonigarcia.github.io/) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package io.github.bonigarcia; 18 | 19 | import java.util.List; 20 | import java.util.Optional; 21 | 22 | import org.slf4j.Logger; 23 | import org.slf4j.LoggerFactory; 24 | import org.springframework.stereotype.Service; 25 | 26 | @Service 27 | public class CatService { 28 | 29 | private static final double MIN_STARS = 0.5; 30 | private static final double MAX_STARS = 5; 31 | 32 | final Logger log = LoggerFactory.getLogger(CatService.class); 33 | 34 | private CatRepository catRepository; 35 | 36 | public CatService(CatRepository catRepository) { 37 | this.catRepository = catRepository; 38 | } 39 | 40 | public Cat saveCat(Cat cat) { 41 | return catRepository.saveAndFlush(cat); 42 | } 43 | 44 | public Cat rateCat(double stars, String comment, long catId) { 45 | Optional optionalCat = catRepository.findById(catId); 46 | if (!optionalCat.isPresent()) { 47 | throw new CatException("Cat with id " + catId + " not available"); 48 | } 49 | rateCat(stars, comment, optionalCat.get()); 50 | return optionalCat.get(); 51 | } 52 | 53 | public Cat rateCat(double stars, Cat cat) { 54 | return rateCat(stars, "", cat); 55 | } 56 | 57 | public Cat rateCat(double stars, String comment, Cat cat) { 58 | log.debug("Rating cat with {} stars and comment: \"{}\"", stars, 59 | comment); 60 | 61 | if (stars < MIN_STARS) { 62 | throw new CatException( 63 | "The minimum number of possible stars is " + MIN_STARS); 64 | } 65 | if (stars > MAX_STARS) { 66 | throw new CatException( 67 | "The maximum number of possible stars is " + MAX_STARS); 68 | } 69 | 70 | cat.rate(stars, comment); 71 | saveCat(cat); 72 | log.debug("Cat info after rating: {}", cat); 73 | 74 | return cat; 75 | } 76 | 77 | public long getCatCount() { 78 | return catRepository.findAll().spliterator().getExactSizeIfKnown(); 79 | } 80 | 81 | public List getAllCats() { 82 | return catRepository.findAll(); 83 | } 84 | 85 | public List getOpinions(Cat cat) { 86 | Optional optionalCat = catRepository.findById(cat.getId()); 87 | if (!optionalCat.isPresent()) { 88 | throw new CatException("Cat not available: " + cat); 89 | } 90 | return optionalCat.get().getOpinions(); 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/io/github/bonigarcia/CookiesService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2017 Boni Garcia (https://bonigarcia.github.io/) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package io.github.bonigarcia; 18 | 19 | import java.util.ArrayList; 20 | import java.util.Base64; 21 | import java.util.List; 22 | import java.util.Optional; 23 | 24 | import org.slf4j.Logger; 25 | import org.slf4j.LoggerFactory; 26 | import org.springframework.stereotype.Service; 27 | 28 | import jakarta.servlet.http.Cookie; 29 | import jakarta.servlet.http.HttpServletResponse; 30 | 31 | @Service 32 | public class CookiesService { 33 | 34 | public static final String COOKIE_NAME = "catList"; 35 | public static final String VALUE_SEPARATOR = "#"; 36 | public static final String CAT_SEPARATOR = "_"; 37 | 38 | final Logger log = LoggerFactory.getLogger(CookiesService.class); 39 | 40 | public String updateCookies(String cookieValue, Long catId, Double stars, 41 | String comment, HttpServletResponse response) { 42 | String newCookieValue = cookieValue + catId + VALUE_SEPARATOR + stars 43 | + VALUE_SEPARATOR 44 | + Base64.getEncoder().encodeToString(comment.getBytes()) 45 | + CAT_SEPARATOR; 46 | 47 | log.debug("Adding cookie {}={}", COOKIE_NAME, newCookieValue); 48 | response.addCookie(new Cookie(COOKIE_NAME, newCookieValue)); 49 | 50 | return newCookieValue; 51 | } 52 | 53 | public boolean isCatInCookies(Cat cat, String cookieValue) { 54 | String[] cats = cookieValue.split(CAT_SEPARATOR); 55 | for (String strCat : cats) { 56 | if (strCat.equals("")) { 57 | continue; 58 | } 59 | if (cat.getId() == Long 60 | .parseLong(strCat.split(VALUE_SEPARATOR)[0])) { 61 | return true; 62 | } 63 | } 64 | return false; 65 | } 66 | 67 | public List updateOpinionsWithCookiesValue(Cat cat, 68 | String cookieValue) { 69 | List outputOpinionList = new ArrayList<>(); 70 | Optional cookieValueForCat = getValueForCat(cat, cookieValue); 71 | 72 | if (cookieValueForCat.isPresent()) { 73 | String[] split = cookieValueForCat.get().split(VALUE_SEPARATOR); 74 | double stars = Double.parseDouble(split[1]); 75 | String comment = split.length > 2 76 | ? new String(Base64.getDecoder().decode(split[2])) 77 | : ""; 78 | 79 | boolean opinionInCookies = false; 80 | for (Opinion opinion : cat.getOpinions()) { 81 | opinionInCookies = isOpinionInCookies(opinion, stars, comment); 82 | opinion.setInCookies(opinionInCookies); 83 | outputOpinionList.add(opinion); 84 | } 85 | 86 | if (!opinionInCookies) { 87 | Opinion opinion = new Opinion(stars, comment); 88 | opinion.setInCookies(true); 89 | outputOpinionList.add(opinion); 90 | } 91 | } 92 | return outputOpinionList; 93 | } 94 | 95 | public boolean isOpinionInCookies(Opinion opinion, double stars, 96 | String comment) { 97 | return opinion.getStars() == stars 98 | && opinion.getComment().equals(comment); 99 | } 100 | 101 | public Optional getValueForCat(Cat cat, String cookieValue) { 102 | String[] cats = cookieValue.split(CAT_SEPARATOR); 103 | for (String strCat : cats) { 104 | if (strCat.equals("")) { 105 | continue; 106 | } 107 | if (cat.getId() == Long 108 | .parseLong(strCat.split(VALUE_SEPARATOR)[0])) { 109 | return Optional.of(strCat); 110 | } 111 | } 112 | return Optional.empty(); 113 | } 114 | 115 | public List filterCatListWithCookies(Iterable allCats, 116 | String cookieValue) { 117 | List filteredCats = new ArrayList<>(); 118 | allCats.forEach(cat -> { 119 | boolean catInCookies = isCatInCookies(cat, cookieValue); 120 | cat.setInCookies(catInCookies); 121 | 122 | if (catInCookies) { 123 | cat.setOpinions( 124 | updateOpinionsWithCookiesValue(cat, cookieValue)); 125 | } 126 | 127 | filteredCats.add(cat); 128 | log.trace("Cat: {}", cat); 129 | }); 130 | return filteredCats; 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /src/main/java/io/github/bonigarcia/DatabasePopulator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2017 Boni Garcia (https://bonigarcia.github.io/) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package io.github.bonigarcia; 18 | 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | 22 | import org.slf4j.Logger; 23 | import org.slf4j.LoggerFactory; 24 | import org.springframework.beans.factory.annotation.Autowired; 25 | import org.springframework.stereotype.Component; 26 | 27 | import jakarta.annotation.PostConstruct; 28 | 29 | @Component 30 | public class DatabasePopulator { 31 | 32 | @Autowired 33 | private CatService catService; 34 | 35 | final Logger log = LoggerFactory.getLogger(DatabasePopulator.class); 36 | 37 | @PostConstruct 38 | private void initDatabase() { 39 | 40 | if (catService.getAllCats().isEmpty()) { 41 | log.debug("Pupulating database with cats"); 42 | 43 | Cat baby = new Cat("Baby", "baby.jpg"); 44 | List babyOpinions = new ArrayList<>(); 45 | babyOpinions.add(new Opinion(5, "Very cute")); 46 | babyOpinions.add(new Opinion(3, "So so :|")); 47 | baby.setOpinions(babyOpinions); 48 | catService.saveCat(baby); 49 | 50 | Cat bella = new Cat("Bella", "bella.jpg"); 51 | List bellaOpinions = new ArrayList<>(); 52 | bellaOpinions.add(new Opinion(5, "Simply amazing")); 53 | bellaOpinions.add(new Opinion(4.5F, "That's a wonderful cat")); 54 | bellaOpinions.add(new Opinion(4, "I like this one :)")); 55 | bellaOpinions.add(new Opinion(3, "Not bad")); 56 | bella.setOpinions(bellaOpinions); 57 | catService.saveCat(bella); 58 | 59 | catService.saveCat(new Cat("Gizmo", "gizmo.jpg")); 60 | catService.saveCat(new Cat("Kitty", "kitty.jpg")); 61 | catService.saveCat(new Cat("Luna", "luna.jpg")); 62 | catService.saveCat(new Cat("Shadow", "shadow.jpg")); 63 | catService.saveCat(new Cat("Smokey", "smokey.jpg")); 64 | catService.saveCat(new Cat("Tigger", "tigger.jpg")); 65 | catService.saveCat(new Cat("Toby", "toby.jpg")); 66 | } 67 | 68 | log.debug("Number of cats in the database: {}", 69 | catService.getCatCount()); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/io/github/bonigarcia/Opinion.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2017 Boni Garcia (https://bonigarcia.github.io/) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package io.github.bonigarcia; 18 | 19 | import jakarta.persistence.Entity; 20 | import jakarta.persistence.GeneratedValue; 21 | import jakarta.persistence.GenerationType; 22 | import jakarta.persistence.Id; 23 | import jakarta.persistence.Transient; 24 | 25 | @Entity 26 | public class Opinion { 27 | 28 | @Id 29 | @GeneratedValue(strategy = GenerationType.AUTO) 30 | private long id; 31 | 32 | private double stars; 33 | private String comment; 34 | 35 | @Transient 36 | private boolean inCookies; 37 | 38 | protected Opinion() { 39 | } 40 | 41 | protected Opinion(double stars, String comment) { 42 | this.stars = stars; 43 | this.comment = comment; 44 | } 45 | 46 | public double getHalfRoundedStars() { 47 | return Math.round(stars / 0.5) * 0.5; 48 | } 49 | 50 | public double getStars() { 51 | return stars; 52 | } 53 | 54 | public String getComment() { 55 | return comment; 56 | } 57 | 58 | public boolean isInCookies() { 59 | return inCookies; 60 | } 61 | 62 | public void setInCookies(boolean inCookies) { 63 | this.inCookies = inCookies; 64 | } 65 | 66 | @Override 67 | public String toString() { 68 | return "Opinion [id=" + id + ", stars=" + stars + ", comment=" + comment 69 | + ", inCookies=" + inCookies + "]"; 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/io/github/bonigarcia/RateMyCatWebApp.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2017 Boni Garcia (https://bonigarcia.github.io/) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package io.github.bonigarcia; 18 | 19 | import org.springframework.boot.SpringApplication; 20 | import org.springframework.boot.autoconfigure.SpringBootApplication; 21 | 22 | @SpringBootApplication 23 | public class RateMyCatWebApp { 24 | 25 | public static void main(String[] args) { 26 | SpringApplication.run(RateMyCatWebApp.class, args); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/io/github/bonigarcia/WebController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2017 Boni Garcia (https://bonigarcia.github.io/) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package io.github.bonigarcia; 18 | 19 | import static io.github.bonigarcia.CookiesService.COOKIE_NAME; 20 | 21 | import java.util.List; 22 | import java.util.Locale; 23 | 24 | import org.slf4j.Logger; 25 | import org.slf4j.LoggerFactory; 26 | import org.springframework.stereotype.Controller; 27 | import org.springframework.web.bind.annotation.CookieValue; 28 | import org.springframework.web.bind.annotation.GetMapping; 29 | import org.springframework.web.bind.annotation.PostMapping; 30 | import org.springframework.web.bind.annotation.RequestParam; 31 | import org.springframework.web.servlet.ModelAndView; 32 | 33 | import jakarta.servlet.http.HttpServletResponse; 34 | 35 | @Controller 36 | public class WebController { 37 | 38 | private CatService catService; 39 | private CookiesService cookiesService; 40 | 41 | public WebController(CatService catService, CookiesService cookiesService) { 42 | this.catService = catService; 43 | this.cookiesService = cookiesService; 44 | } 45 | 46 | final Logger log = LoggerFactory.getLogger(WebController.class); 47 | 48 | @GetMapping("/") 49 | public ModelAndView index( 50 | @CookieValue(value = COOKIE_NAME, defaultValue = "") String cookiesValue) { 51 | log.trace("Cookies: {}", cookiesValue); 52 | ModelAndView model = new ModelAndView("index"); 53 | List allCats = catService.getAllCats(); 54 | model.addObject("cats", 55 | cookiesService.filterCatListWithCookies(allCats, cookiesValue)); 56 | return model; 57 | } 58 | 59 | @PostMapping("/") 60 | public ModelAndView rate(@RequestParam Long catId, 61 | @RequestParam(required = false) Double stars, 62 | @RequestParam String comment, 63 | @CookieValue(value = COOKIE_NAME, defaultValue = "") String cookiesValue, 64 | HttpServletResponse response) { 65 | log.info("Received vote for cat {}: stars={} comment={}", catId, stars, 66 | comment); 67 | 68 | ModelAndView model = new ModelAndView("index"); 69 | String newCookiesValue = cookiesValue; 70 | try { 71 | if (stars == null) { 72 | model.addObject("errorMessage", 73 | "You need to select some stars for rating each cat"); 74 | 75 | } else { 76 | Cat ratedCat = catService.rateCat(stars, comment, catId); 77 | String sucessMessage = String.format(Locale.US, 78 | "Your vote for %s with %.1f stars and comment '%s' has been stored", 79 | ratedCat.getName(), stars, comment); 80 | model.addObject("sucessMessage", sucessMessage); 81 | 82 | newCookiesValue = cookiesService.updateCookies(cookiesValue, 83 | catId, stars, comment, response); 84 | } 85 | } catch (Exception e) { 86 | log.error("Exception rating cat: {}", e.getMessage()); 87 | model.addObject("errorMessage", e.getMessage()); 88 | } finally { 89 | List allCats = catService.getAllCats(); 90 | model.addObject("cats", cookiesService 91 | .filterCatListWithCookies(allCats, newCookiesValue)); 92 | } 93 | 94 | return model; 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # Store H2 in local file system and allow other simultaneous connections 2 | #spring.datasource.url=jdbc:h2:~/.h2/rate-my-cat-ddbb;AUTO_SERVER=TRUE 3 | 4 | # See: https://stackoverflow.com/questions/9318116/how-to-run-h2-database-in-server-mode 5 | #tcpPort=30412 6 | 7 | # Keep the DDL 8 | #spring.jpa.hibernate.ddl-auto=update 9 | 10 | # Credentials for database 11 | #spring.datasource.username=root 12 | #spring.datasource.password=toor 13 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/main/resources/static/css/carousel.css: -------------------------------------------------------------------------------- 1 | .carousel-indicators .active { 2 | background: #31708f; 3 | } 4 | 5 | .content { 6 | margin-top: 20px; 7 | } 8 | 9 | .adjust1 { 10 | float: left; 11 | width: 100%; 12 | margin-bottom: 0; 13 | } 14 | 15 | .adjust2 { 16 | margin: 0; 17 | } 18 | 19 | .carousel-indicators li { 20 | border: 1px solid #ccc; 21 | } 22 | 23 | .carousel-control { 24 | color: #31708f; 25 | width: 5%; 26 | } 27 | 28 | .carousel-control:hover, .carousel-control:focus { 29 | color: #31708f; 30 | } 31 | 32 | .carousel-control.left, .carousel-control.right { 33 | background-image: none; 34 | } 35 | 36 | .media-object { 37 | margin: auto; 38 | margin-top: 15%; 39 | } 40 | 41 | @media screen and (max-width: 768px) { 42 | .media-object { 43 | margin-top: 0; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/resources/static/css/gallery-grid.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-image: linear-gradient(to top, #ecedee 0%, #eceeef 75%, #e7e8e9 100%); 3 | min-height: 100vh; 4 | font: normal 16px sans-serif; 5 | padding: 60px 0; 6 | } 7 | 8 | .container.gallery-container { 9 | background-color: #fff; 10 | color: #35373a; 11 | min-height: 100vh; 12 | border-radius: 20px; 13 | box-shadow: 0 8px 15px rgba(0, 0, 0, 0.06); 14 | } 15 | 16 | .gallery-container h1 { 17 | text-align: center; 18 | margin-top: 70px; 19 | font-family: 'Droid Sans', sans-serif; 20 | font-weight: bold; 21 | } 22 | 23 | .gallery-container p.page-description { 24 | text-align: center; 25 | max-width: 800px; 26 | margin: 25px auto; 27 | color: #888; 28 | font-size: 18px; 29 | } 30 | 31 | .tz-gallery { 32 | padding: 40px; 33 | } 34 | 35 | .tz-gallery .lightbox img { 36 | width: 100%; 37 | margin-bottom: 30px; 38 | transition: 0.2s ease-in-out; 39 | box-shadow: 0 2px 3px rgba(0,0,0,0.2); 40 | } 41 | 42 | 43 | .tz-gallery .lightbox img:hover { 44 | transform: scale(1.05); 45 | box-shadow: 0 8px 15px rgba(0,0,0,0.3); 46 | } 47 | 48 | .tz-gallery img { 49 | border-radius: 4px; 50 | } 51 | 52 | .baguetteBox-button { 53 | background-color: transparent !important; 54 | } 55 | 56 | 57 | @media(max-width: 768px) { 58 | body { 59 | padding: 0; 60 | } 61 | 62 | .container.gallery-container { 63 | border-radius: 0; 64 | } 65 | } -------------------------------------------------------------------------------- /src/main/resources/static/css/star-rating.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * bootstrap-star-rating v4.0.2 3 | * http://plugins.krajee.com/star-rating 4 | * 5 | * Author: Kartik Visweswaran 6 | * Copyright: 2013 - 2017, Kartik Visweswaran, Krajee.com 7 | * 8 | * Licensed under the BSD 3-Clause 9 | * https://github.com/kartik-v/bootstrap-star-rating/blob/master/LICENSE.md 10 | */ 11 | .rating-loading { 12 | width: 25px; 13 | height: 25px; 14 | font-size: 0; 15 | color: #fff; 16 | background: transparent url('../img/loading.gif') top left no-repeat; 17 | border: none; 18 | } 19 | 20 | /* 21 | * Stars & Input 22 | */ 23 | .rating-container .rating-stars { 24 | position: relative; 25 | cursor: pointer; 26 | vertical-align: middle; 27 | display: inline-block; 28 | overflow: hidden; 29 | white-space: nowrap; 30 | } 31 | 32 | .rating-container .rating-input { 33 | position: absolute; 34 | cursor: pointer; 35 | width: 100%; 36 | height: 1px; 37 | bottom: 0; 38 | left: 0; 39 | font-size: 1px; 40 | border: none; 41 | background: none; 42 | padding: 0; 43 | margin: 0; 44 | } 45 | 46 | .rating-disabled .rating-input, .rating-disabled .rating-stars { 47 | cursor: not-allowed; 48 | } 49 | 50 | .rating-container .star { 51 | display: inline-block; 52 | margin: 0 3px; 53 | text-align: center; 54 | } 55 | 56 | .rating-container .empty-stars { 57 | color: #aaa; 58 | } 59 | 60 | .rating-container .filled-stars { 61 | position: absolute; 62 | left: 0; 63 | top: 0; 64 | margin: auto; 65 | color: #fde16d; 66 | white-space: nowrap; 67 | overflow: hidden; 68 | -webkit-text-stroke: 1px #777; 69 | text-shadow: 1px 1px #999; 70 | } 71 | 72 | .rating-rtl { 73 | float: right; 74 | } 75 | 76 | .rating-animate .filled-stars { 77 | transition: width 0.25s ease; 78 | -o-transition: width 0.25s ease; 79 | -moz-transition: width 0.25s ease; 80 | -webkit-transition: width 0.25s ease; 81 | } 82 | 83 | .rating-rtl .filled-stars { 84 | left: auto; 85 | right: 0; 86 | -moz-transform: matrix(-1, 0, 0, 1, 0, 0) translate3d(0, 0, 0); 87 | -webkit-transform: matrix(-1, 0, 0, 1, 0, 0) translate3d(0, 0, 0); 88 | -o-transform: matrix(-1, 0, 0, 1, 0, 0) translate3d(0, 0, 0); 89 | transform: matrix(-1, 0, 0, 1, 0, 0) translate3d(0, 0, 0); 90 | } 91 | 92 | .rating-rtl.is-star .filled-stars { 93 | right: 0.06em; 94 | } 95 | 96 | .rating-rtl.is-heart .empty-stars { 97 | margin-right: 0.07em; 98 | } 99 | 100 | /** 101 | * Sizes 102 | */ 103 | .rating-xl { 104 | font-size: 4.89em; 105 | } 106 | 107 | .rating-lg { 108 | font-size: 3.91em; 109 | } 110 | 111 | .rating-md { 112 | font-size: 3.13em; 113 | } 114 | 115 | .rating-sm { 116 | font-size: 2.5em; 117 | } 118 | 119 | .rating-xs { 120 | font-size: 2em; 121 | } 122 | 123 | .rating-xl { 124 | font-size: 4.89em; 125 | } 126 | 127 | /** 128 | * Clear 129 | */ 130 | .rating-container .clear-rating { 131 | color: #aaa; 132 | cursor: not-allowed; 133 | display: inline-block; 134 | vertical-align: middle; 135 | font-size: 60%; 136 | } 137 | 138 | .clear-rating-active { 139 | cursor: pointer !important; 140 | } 141 | 142 | .clear-rating-active:hover { 143 | color: #843534; 144 | } 145 | 146 | .rating-container .clear-rating { 147 | padding-right: 5px; 148 | } 149 | 150 | /** 151 | * Caption 152 | */ 153 | .rating-container .caption { 154 | color: #999; 155 | display: inline-block; 156 | vertical-align: middle; 157 | font-size: 60%; 158 | margin-top: -0.6em; 159 | } 160 | 161 | .rating-container .caption { 162 | margin-left: 5px; 163 | margin-right: 0; 164 | } 165 | 166 | .rating-rtl .caption { 167 | margin-right: 5px; 168 | margin-left: 0; 169 | } 170 | 171 | /** 172 | * Print 173 | */ 174 | @media print { 175 | .rating-container .clear-rating { 176 | display: none; 177 | } 178 | } -------------------------------------------------------------------------------- /src/main/resources/static/img/baby.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bonigarcia/rate-my-cat/c244829e2ff07967fd6d1a5484c7e5bcefc48f7a/src/main/resources/static/img/baby.jpg -------------------------------------------------------------------------------- /src/main/resources/static/img/bella.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bonigarcia/rate-my-cat/c244829e2ff07967fd6d1a5484c7e5bcefc48f7a/src/main/resources/static/img/bella.jpg -------------------------------------------------------------------------------- /src/main/resources/static/img/gizmo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bonigarcia/rate-my-cat/c244829e2ff07967fd6d1a5484c7e5bcefc48f7a/src/main/resources/static/img/gizmo.jpg -------------------------------------------------------------------------------- /src/main/resources/static/img/kitty.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bonigarcia/rate-my-cat/c244829e2ff07967fd6d1a5484c7e5bcefc48f7a/src/main/resources/static/img/kitty.jpg -------------------------------------------------------------------------------- /src/main/resources/static/img/luna.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bonigarcia/rate-my-cat/c244829e2ff07967fd6d1a5484c7e5bcefc48f7a/src/main/resources/static/img/luna.jpg -------------------------------------------------------------------------------- /src/main/resources/static/img/rate-my-cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bonigarcia/rate-my-cat/c244829e2ff07967fd6d1a5484c7e5bcefc48f7a/src/main/resources/static/img/rate-my-cat.png -------------------------------------------------------------------------------- /src/main/resources/static/img/shadow.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bonigarcia/rate-my-cat/c244829e2ff07967fd6d1a5484c7e5bcefc48f7a/src/main/resources/static/img/shadow.jpg -------------------------------------------------------------------------------- /src/main/resources/static/img/smokey.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bonigarcia/rate-my-cat/c244829e2ff07967fd6d1a5484c7e5bcefc48f7a/src/main/resources/static/img/smokey.jpg -------------------------------------------------------------------------------- /src/main/resources/static/img/stars-0.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bonigarcia/rate-my-cat/c244829e2ff07967fd6d1a5484c7e5bcefc48f7a/src/main/resources/static/img/stars-0.0.png -------------------------------------------------------------------------------- /src/main/resources/static/img/stars-0.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bonigarcia/rate-my-cat/c244829e2ff07967fd6d1a5484c7e5bcefc48f7a/src/main/resources/static/img/stars-0.5.png -------------------------------------------------------------------------------- /src/main/resources/static/img/stars-1.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bonigarcia/rate-my-cat/c244829e2ff07967fd6d1a5484c7e5bcefc48f7a/src/main/resources/static/img/stars-1.0.png -------------------------------------------------------------------------------- /src/main/resources/static/img/stars-1.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bonigarcia/rate-my-cat/c244829e2ff07967fd6d1a5484c7e5bcefc48f7a/src/main/resources/static/img/stars-1.5.png -------------------------------------------------------------------------------- /src/main/resources/static/img/stars-2.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bonigarcia/rate-my-cat/c244829e2ff07967fd6d1a5484c7e5bcefc48f7a/src/main/resources/static/img/stars-2.0.png -------------------------------------------------------------------------------- /src/main/resources/static/img/stars-2.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bonigarcia/rate-my-cat/c244829e2ff07967fd6d1a5484c7e5bcefc48f7a/src/main/resources/static/img/stars-2.5.png -------------------------------------------------------------------------------- /src/main/resources/static/img/stars-3.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bonigarcia/rate-my-cat/c244829e2ff07967fd6d1a5484c7e5bcefc48f7a/src/main/resources/static/img/stars-3.0.png -------------------------------------------------------------------------------- /src/main/resources/static/img/stars-3.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bonigarcia/rate-my-cat/c244829e2ff07967fd6d1a5484c7e5bcefc48f7a/src/main/resources/static/img/stars-3.5.png -------------------------------------------------------------------------------- /src/main/resources/static/img/stars-4.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bonigarcia/rate-my-cat/c244829e2ff07967fd6d1a5484c7e5bcefc48f7a/src/main/resources/static/img/stars-4.0.png -------------------------------------------------------------------------------- /src/main/resources/static/img/stars-4.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bonigarcia/rate-my-cat/c244829e2ff07967fd6d1a5484c7e5bcefc48f7a/src/main/resources/static/img/stars-4.5.png -------------------------------------------------------------------------------- /src/main/resources/static/img/stars-5.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bonigarcia/rate-my-cat/c244829e2ff07967fd6d1a5484c7e5bcefc48f7a/src/main/resources/static/img/stars-5.0.png -------------------------------------------------------------------------------- /src/main/resources/static/img/tigger.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bonigarcia/rate-my-cat/c244829e2ff07967fd6d1a5484c7e5bcefc48f7a/src/main/resources/static/img/tigger.jpg -------------------------------------------------------------------------------- /src/main/resources/static/img/toby.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bonigarcia/rate-my-cat/c244829e2ff07967fd6d1a5484c7e5bcefc48f7a/src/main/resources/static/img/toby.jpg -------------------------------------------------------------------------------- /src/main/resources/static/js/star-rating.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * bootstrap-star-rating v4.0.2 3 | * http://plugins.krajee.com/star-rating 4 | * 5 | * Author: Kartik Visweswaran 6 | * Copyright: 2013 - 2017, Kartik Visweswaran, Krajee.com 7 | * 8 | * Licensed under the BSD 3-Clause 9 | * https://github.com/kartik-v/bootstrap-star-rating/blob/master/LICENSE.md 10 | */ 11 | (function (factory) { 12 | "use strict"; 13 | //noinspection JSUnresolvedVariable 14 | if (typeof define === 'function' && define.amd) { // jshint ignore:line 15 | // AMD. Register as an anonymous module. 16 | define(['jquery'], factory); // jshint ignore:line 17 | } else { // noinspection JSUnresolvedVariable 18 | if (typeof module === 'object' && module.exports) { // jshint ignore:line 19 | // Node/CommonJS 20 | // noinspection JSUnresolvedVariable 21 | module.exports = factory(require('jquery')); // jshint ignore:line 22 | } else { 23 | // Browser globals 24 | factory(window.jQuery); 25 | } 26 | } 27 | }(function ($) { 28 | "use strict"; 29 | 30 | $.fn.ratingLocales = {}; 31 | $.fn.ratingThemes = {}; 32 | 33 | var $h, Rating; 34 | 35 | // global helper methods and constants 36 | $h = { 37 | NAMESPACE: '.rating', 38 | DEFAULT_MIN: 0, 39 | DEFAULT_MAX: 5, 40 | DEFAULT_STEP: 0.5, 41 | isEmpty: function (value, trim) { 42 | return value === null || value === undefined || value.length === 0 || (trim && $.trim(value) === ''); 43 | }, 44 | getCss: function (condition, css) { 45 | return condition ? ' ' + css : ''; 46 | }, 47 | addCss: function ($el, css) { 48 | $el.removeClass(css).addClass(css); 49 | }, 50 | getDecimalPlaces: function (num) { 51 | var m = ('' + num).match(/(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/); 52 | return !m ? 0 : Math.max(0, (m[1] ? m[1].length : 0) - (m[2] ? +m[2] : 0)); 53 | }, 54 | applyPrecision: function (val, precision) { 55 | return parseFloat(val.toFixed(precision)); 56 | }, 57 | handler: function ($el, event, callback, skipOff, skipNS) { 58 | var ev = skipNS ? event : event.split(' ').join($h.NAMESPACE + ' ') + $h.NAMESPACE; 59 | if (!skipOff) { 60 | $el.off(ev); 61 | } 62 | $el.on(ev, callback); 63 | } 64 | }; 65 | 66 | // rating constructor 67 | Rating = function (element, options) { 68 | var self = this; 69 | self.$element = $(element); 70 | self._init(options); 71 | }; 72 | Rating.prototype = { 73 | constructor: Rating, 74 | _parseAttr: function (vattr, options) { 75 | var self = this, $el = self.$element, elType = $el.attr('type'), finalVal, val, chk, out; 76 | if (elType === 'range' || elType === 'number') { 77 | val = options[vattr] || $el.data(vattr) || $el.attr(vattr); 78 | switch (vattr) { 79 | case 'min': 80 | chk = $h.DEFAULT_MIN; 81 | break; 82 | case 'max': 83 | chk = $h.DEFAULT_MAX; 84 | break; 85 | default: 86 | chk = $h.DEFAULT_STEP; 87 | } 88 | finalVal = $h.isEmpty(val) ? chk : val; 89 | out = parseFloat(finalVal); 90 | } else { 91 | out = parseFloat(options[vattr]); 92 | } 93 | return isNaN(out) ? chk : out; 94 | }, 95 | _parseValue: function (val) { 96 | var self = this, v = parseFloat(val); 97 | if (isNaN(v)) { 98 | v = self.clearValue; 99 | } 100 | return (self.zeroAsNull && (v === 0 || v === '0') ? null : v); 101 | }, 102 | _setDefault: function (key, val) { 103 | var self = this; 104 | if ($h.isEmpty(self[key])) { 105 | self[key] = val; 106 | } 107 | }, 108 | _initSlider: function (options) { 109 | var self = this, v = self.$element.val(); 110 | self.initialValue = $h.isEmpty(v) ? 0 : v; 111 | self._setDefault('min', self._parseAttr('min', options)); 112 | self._setDefault('max', self._parseAttr('max', options)); 113 | self._setDefault('step', self._parseAttr('step', options)); 114 | if (isNaN(self.min) || $h.isEmpty(self.min)) { 115 | self.min = $h.DEFAULT_MIN; 116 | } 117 | if (isNaN(self.max) || $h.isEmpty(self.max)) { 118 | self.max = $h.DEFAULT_MAX; 119 | } 120 | if (isNaN(self.step) || $h.isEmpty(self.step) || self.step === 0) { 121 | self.step = $h.DEFAULT_STEP; 122 | } 123 | self.diff = self.max - self.min; 124 | }, 125 | _initHighlight: function (v) { 126 | var self = this, w, cap = self._getCaption(); 127 | if (!v) { 128 | v = self.$element.val(); 129 | } 130 | w = self.getWidthFromValue(v) + '%'; 131 | self.$filledStars.width(w); 132 | self.cache = {caption: cap, width: w, val: v}; 133 | }, 134 | _getContainerCss: function () { 135 | var self = this; 136 | return 'rating-container' + 137 | $h.getCss(self.theme, 'theme-' + self.theme) + 138 | $h.getCss(self.rtl, 'rating-rtl') + 139 | $h.getCss(self.size, 'rating-' + self.size) + 140 | $h.getCss(self.animate, 'rating-animate') + 141 | $h.getCss(self.disabled || self.readonly, 'rating-disabled') + 142 | $h.getCss(self.containerClass, self.containerClass); 143 | }, 144 | _checkDisabled: function () { 145 | var self = this, $el = self.$element, opts = self.options; 146 | self.disabled = opts.disabled === undefined ? $el.attr('disabled') || false : opts.disabled; 147 | self.readonly = opts.readonly === undefined ? $el.attr('readonly') || false : opts.readonly; 148 | self.inactive = (self.disabled || self.readonly); 149 | $el.attr({disabled: self.disabled, readonly: self.readonly}); 150 | }, 151 | _addContent: function (type, content) { 152 | var self = this, $container = self.$container, isClear = type === 'clear'; 153 | if (self.rtl) { 154 | return isClear ? $container.append(content) : $container.prepend(content); 155 | } else { 156 | return isClear ? $container.prepend(content) : $container.append(content); 157 | } 158 | }, 159 | _generateRating: function () { 160 | var self = this, $el = self.$element, $rating, $container, w; 161 | $container = self.$container = $(document.createElement("div")).insertBefore($el); 162 | $h.addCss($container, self._getContainerCss()); 163 | self.$rating = $rating = $(document.createElement("div")).attr('class', 'rating-stars').appendTo($container) 164 | .append(self._getStars('empty')).append(self._getStars('filled')); 165 | self.$emptyStars = $rating.find('.empty-stars'); 166 | self.$filledStars = $rating.find('.filled-stars'); 167 | self._renderCaption(); 168 | self._renderClear(); 169 | self._initHighlight(); 170 | $container.append($el); 171 | if (self.rtl) { 172 | w = Math.max(self.$emptyStars.outerWidth(), self.$filledStars.outerWidth()); 173 | self.$emptyStars.width(w); 174 | } 175 | $el.appendTo($rating); 176 | }, 177 | _getCaption: function () { 178 | var self = this; 179 | return self.$caption && self.$caption.length ? self.$caption.html() : self.defaultCaption; 180 | }, 181 | _setCaption: function (content) { 182 | var self = this; 183 | if (self.$caption && self.$caption.length) { 184 | self.$caption.html(content); 185 | } 186 | }, 187 | _renderCaption: function () { 188 | var self = this, val = self.$element.val(), html, $cap = self.captionElement ? $(self.captionElement) : ''; 189 | if (!self.showCaption) { 190 | return; 191 | } 192 | html = self.fetchCaption(val); 193 | if ($cap && $cap.length) { 194 | $h.addCss($cap, 'caption'); 195 | $cap.html(html); 196 | self.$caption = $cap; 197 | return; 198 | } 199 | self._addContent('caption', '
' + html + '
'); 200 | self.$caption = self.$container.find(".caption"); 201 | }, 202 | _renderClear: function () { 203 | var self = this, css, $clr = self.clearElement ? $(self.clearElement) : ''; 204 | if (!self.showClear) { 205 | return; 206 | } 207 | css = self._getClearClass(); 208 | if ($clr.length) { 209 | $h.addCss($clr, css); 210 | $clr.attr({"title": self.clearButtonTitle}).html(self.clearButton); 211 | self.$clear = $clr; 212 | return; 213 | } 214 | self._addContent('clear', 215 | '
' + self.clearButton + '
'); 216 | self.$clear = self.$container.find('.' + self.clearButtonBaseClass); 217 | }, 218 | _getClearClass: function () { 219 | var self = this; 220 | return self.clearButtonBaseClass + ' ' + (self.inactive ? '' : self.clearButtonActiveClass); 221 | }, 222 | _toggleHover: function (out) { 223 | var self = this, w, width, caption; 224 | if (!out) { 225 | return; 226 | } 227 | if (self.hoverChangeStars) { 228 | w = self.getWidthFromValue(self.clearValue); 229 | width = out.val <= self.clearValue ? w + '%' : out.width; 230 | self.$filledStars.css('width', width); 231 | } 232 | if (self.hoverChangeCaption) { 233 | caption = out.val <= self.clearValue ? self.fetchCaption(self.clearValue) : out.caption; 234 | if (caption) { 235 | self._setCaption(caption + ''); 236 | } 237 | } 238 | }, 239 | _init: function (options) { 240 | var self = this, $el = self.$element.addClass('rating-input'), v; 241 | self.options = options; 242 | $.each(options, function (key, value) { 243 | self[key] = value; 244 | }); 245 | if (self.rtl || $el.attr('dir') === 'rtl') { 246 | self.rtl = true; 247 | $el.attr('dir', 'rtl'); 248 | } 249 | self.starClicked = false; 250 | self.clearClicked = false; 251 | self._initSlider(options); 252 | self._checkDisabled(); 253 | if (self.displayOnly) { 254 | self.inactive = true; 255 | self.showClear = false; 256 | self.showCaption = false; 257 | } 258 | self._generateRating(); 259 | self._initEvents(); 260 | self._listen(); 261 | v = self._parseValue($el.val()); 262 | $el.val(v); 263 | return $el.removeClass('rating-loading'); 264 | }, 265 | _initEvents: function () { 266 | var self = this; 267 | self.events = { 268 | _getTouchPosition: function (e) { 269 | var pageX = $h.isEmpty(e.pageX) ? e.originalEvent.touches[0].pageX : e.pageX; 270 | return pageX - self.$rating.offset().left; 271 | }, 272 | _listenClick: function (e, callback) { 273 | e.stopPropagation(); 274 | e.preventDefault(); 275 | if (e.handled !== true) { 276 | callback(e); 277 | e.handled = true; 278 | } else { 279 | return false; 280 | } 281 | }, 282 | _noMouseAction: function (e) { 283 | return !self.hoverEnabled || self.inactive || (e && e.isDefaultPrevented()); 284 | }, 285 | initTouch: function (e) { 286 | //noinspection JSUnresolvedVariable 287 | var ev, touches, pos, out, caption, w, width, params, clrVal = self.clearValue || 0, 288 | isTouchCapable = 'ontouchstart' in window || 289 | (window.DocumentTouch && document instanceof window.DocumentTouch); 290 | if (!isTouchCapable || self.inactive) { 291 | return; 292 | } 293 | ev = e.originalEvent; 294 | //noinspection JSUnresolvedVariable 295 | touches = !$h.isEmpty(ev.touches) ? ev.touches : ev.changedTouches; 296 | pos = self.events._getTouchPosition(touches[0]); 297 | if (e.type === "touchend") { 298 | self._setStars(pos); 299 | params = [self.$element.val(), self._getCaption()]; 300 | self.$element.trigger('change').trigger('rating.change', params); 301 | self.starClicked = true; 302 | } else { 303 | out = self.calculate(pos); 304 | caption = out.val <= clrVal ? self.fetchCaption(clrVal) : out.caption; 305 | w = self.getWidthFromValue(clrVal); 306 | width = out.val <= clrVal ? w + '%' : out.width; 307 | self._setCaption(caption); 308 | self.$filledStars.css('width', width); 309 | } 310 | }, 311 | starClick: function (e) { 312 | var pos, params; 313 | self.events._listenClick(e, function (ev) { 314 | if (self.inactive) { 315 | return false; 316 | } 317 | pos = self.events._getTouchPosition(ev); 318 | self._setStars(pos); 319 | params = [self.$element.val(), self._getCaption()]; 320 | self.$element.trigger('change').trigger('rating.change', params); 321 | self.starClicked = true; 322 | }); 323 | }, 324 | clearClick: function (e) { 325 | self.events._listenClick(e, function () { 326 | if (!self.inactive) { 327 | self.clear(); 328 | self.clearClicked = true; 329 | } 330 | }); 331 | }, 332 | starMouseMove: function (e) { 333 | var pos, out; 334 | if (self.events._noMouseAction(e)) { 335 | return; 336 | } 337 | self.starClicked = false; 338 | pos = self.events._getTouchPosition(e); 339 | out = self.calculate(pos); 340 | self._toggleHover(out); 341 | self.$element.trigger('rating.hover', [out.val, out.caption, 'stars']); 342 | }, 343 | starMouseLeave: function (e) { 344 | var out; 345 | if (self.events._noMouseAction(e) || self.starClicked) { 346 | return; 347 | } 348 | out = self.cache; 349 | self._toggleHover(out); 350 | self.$element.trigger('rating.hoverleave', ['stars']); 351 | }, 352 | clearMouseMove: function (e) { 353 | var caption, val, width, out; 354 | if (self.events._noMouseAction(e) || !self.hoverOnClear) { 355 | return; 356 | } 357 | self.clearClicked = false; 358 | caption = '' + self.clearCaption + ''; 359 | val = self.clearValue; 360 | width = self.getWidthFromValue(val) || 0; 361 | out = {caption: caption, width: width, val: val}; 362 | self._toggleHover(out); 363 | self.$element.trigger('rating.hover', [val, caption, 'clear']); 364 | }, 365 | clearMouseLeave: function (e) { 366 | var out; 367 | if (self.events._noMouseAction(e) || self.clearClicked || !self.hoverOnClear) { 368 | return; 369 | } 370 | out = self.cache; 371 | self._toggleHover(out); 372 | self.$element.trigger('rating.hoverleave', ['clear']); 373 | }, 374 | resetForm: function (e) { 375 | if (e && e.isDefaultPrevented()) { 376 | return; 377 | } 378 | if (!self.inactive) { 379 | self.reset(); 380 | } 381 | } 382 | }; 383 | }, 384 | _listen: function () { 385 | var self = this, $el = self.$element, $form = $el.closest('form'), $rating = self.$rating, 386 | $clear = self.$clear, events = self.events; 387 | $h.handler($rating, 'touchstart touchmove touchend', $.proxy(events.initTouch, self)); 388 | $h.handler($rating, 'click touchstart', $.proxy(events.starClick, self)); 389 | $h.handler($rating, 'mousemove', $.proxy(events.starMouseMove, self)); 390 | $h.handler($rating, 'mouseleave', $.proxy(events.starMouseLeave, self)); 391 | if (self.showClear && $clear.length) { 392 | $h.handler($clear, 'click touchstart', $.proxy(events.clearClick, self)); 393 | $h.handler($clear, 'mousemove', $.proxy(events.clearMouseMove, self)); 394 | $h.handler($clear, 'mouseleave', $.proxy(events.clearMouseLeave, self)); 395 | } 396 | if ($form.length) { 397 | $h.handler($form, 'reset', $.proxy(events.resetForm, self), true); 398 | } 399 | return $el; 400 | }, 401 | _getStars: function (type) { 402 | var self = this, stars = '', i; 403 | for (i = 1; i <= self.stars; i++) { 404 | stars += '' + self[type + 'Star'] + ''; 405 | } 406 | return stars + ''; 407 | }, 408 | _setStars: function (pos) { 409 | var self = this, out = arguments.length ? self.calculate(pos) : self.calculate(), $el = self.$element, 410 | v = self._parseValue(out.val); 411 | $el.val(v); 412 | self.$filledStars.css('width', out.width); 413 | self._setCaption(out.caption); 414 | self.cache = out; 415 | return $el; 416 | }, 417 | showStars: function (val) { 418 | var self = this, v = self._parseValue(val); 419 | self.$element.val(v); 420 | return self._setStars(); 421 | }, 422 | calculate: function (pos) { 423 | var self = this, defaultVal = $h.isEmpty(self.$element.val()) ? 0 : self.$element.val(), 424 | val = arguments.length ? self.getValueFromPosition(pos) : defaultVal, 425 | caption = self.fetchCaption(val), width = self.getWidthFromValue(val); 426 | width += '%'; 427 | return {caption: caption, width: width, val: val}; 428 | }, 429 | getValueFromPosition: function (pos) { 430 | var self = this, precision = $h.getDecimalPlaces(self.step), val, factor, maxWidth = self.$rating.width(); 431 | factor = (self.diff * pos) / (maxWidth * self.step); 432 | factor = self.rtl ? Math.floor(factor) : Math.ceil(factor); 433 | val = $h.applyPrecision(parseFloat(self.min + factor * self.step), precision); 434 | val = Math.max(Math.min(val, self.max), self.min); 435 | return self.rtl ? (self.max - val) : val; 436 | }, 437 | getWidthFromValue: function (val) { 438 | var self = this, min = self.min, max = self.max, factor, $r = self.$emptyStars, w; 439 | if (!val || val <= min || min === max) { 440 | return 0; 441 | } 442 | w = $r.outerWidth(); 443 | factor = w ? $r.width() / w : 1; 444 | if (val >= max) { 445 | return 100; 446 | } 447 | return (val - min) * factor * 100 / (max - min); 448 | }, 449 | fetchCaption: function (rating) { 450 | var self = this, val = parseFloat(rating) || self.clearValue, css, cap, capVal, cssVal, caption, 451 | vCap = self.starCaptions, vCss = self.starCaptionClasses; 452 | if (val && val !== self.clearValue) { 453 | val = $h.applyPrecision(val, $h.getDecimalPlaces(self.step)); 454 | } 455 | cssVal = typeof vCss === "function" ? vCss(val) : vCss[val]; 456 | capVal = typeof vCap === "function" ? vCap(val) : vCap[val]; 457 | cap = $h.isEmpty(capVal) ? self.defaultCaption.replace(/\{rating}/g, val) : capVal; 458 | css = $h.isEmpty(cssVal) ? self.clearCaptionClass : cssVal; 459 | caption = (val === self.clearValue) ? self.clearCaption : cap; 460 | return '' + caption + ''; 461 | }, 462 | destroy: function () { 463 | var self = this, $el = self.$element; 464 | if (!$h.isEmpty(self.$container)) { 465 | self.$container.before($el).remove(); 466 | } 467 | $.removeData($el.get(0)); 468 | return $el.off('rating').removeClass('rating rating-input'); 469 | }, 470 | create: function (options) { 471 | var self = this, opts = options || self.options || {}; 472 | return self.destroy().rating(opts); 473 | }, 474 | clear: function () { 475 | var self = this, title = '' + self.clearCaption + ''; 476 | if (!self.inactive) { 477 | self._setCaption(title); 478 | } 479 | return self.showStars(self.clearValue).trigger('change').trigger('rating.clear'); 480 | }, 481 | reset: function () { 482 | var self = this; 483 | return self.showStars(self.initialValue).trigger('rating.reset'); 484 | }, 485 | update: function (val) { 486 | var self = this; 487 | return arguments.length ? self.showStars(val) : self.$element; 488 | }, 489 | refresh: function (options) { 490 | var self = this, $el = self.$element; 491 | if (!options) { 492 | return $el; 493 | } 494 | return self.destroy().rating($.extend(true, self.options, options)).trigger('rating.refresh'); 495 | } 496 | }; 497 | 498 | $.fn.rating = function (option) { 499 | var args = Array.apply(null, arguments), retvals = []; 500 | args.shift(); 501 | this.each(function () { 502 | var self = $(this), data = self.data('rating'), options = typeof option === 'object' && option, 503 | theme = options.theme || self.data('theme'), lang = options.language || self.data('language') || 'en', 504 | thm = {}, loc = {}, opts; 505 | if (!data) { 506 | if (theme) { 507 | thm = $.fn.ratingThemes[theme] || {}; 508 | } 509 | if (lang !== 'en' && !$h.isEmpty($.fn.ratingLocales[lang])) { 510 | loc = $.fn.ratingLocales[lang]; 511 | } 512 | opts = $.extend(true, {}, $.fn.rating.defaults, thm, $.fn.ratingLocales.en, loc, options, self.data()); 513 | data = new Rating(this, opts); 514 | self.data('rating', data); 515 | } 516 | 517 | if (typeof option === 'string') { 518 | retvals.push(data[option].apply(data, args)); 519 | } 520 | }); 521 | switch (retvals.length) { 522 | case 0: 523 | return this; 524 | case 1: 525 | return retvals[0] === undefined ? this : retvals[0]; 526 | default: 527 | return retvals; 528 | } 529 | }; 530 | 531 | $.fn.rating.defaults = { 532 | theme: '', 533 | language: 'en', 534 | stars: 5, 535 | filledStar: '', 536 | emptyStar: '', 537 | containerClass: '', 538 | size: 'md', 539 | animate: true, 540 | displayOnly: false, 541 | rtl: false, 542 | showClear: true, 543 | showCaption: true, 544 | starCaptionClasses: { 545 | 0.5: 'label label-danger', 546 | 1: 'label label-danger', 547 | 1.5: 'label label-warning', 548 | 2: 'label label-warning', 549 | 2.5: 'label label-info', 550 | 3: 'label label-info', 551 | 3.5: 'label label-primary', 552 | 4: 'label label-primary', 553 | 4.5: 'label label-success', 554 | 5: 'label label-success' 555 | }, 556 | clearButton: '', 557 | clearButtonBaseClass: 'clear-rating', 558 | clearButtonActiveClass: 'clear-rating-active', 559 | clearCaptionClass: 'label label-default', 560 | clearValue: null, 561 | captionElement: null, 562 | clearElement: null, 563 | hoverEnabled: true, 564 | hoverChangeCaption: true, 565 | hoverChangeStars: true, 566 | hoverOnClear: true, 567 | zeroAsNull: true 568 | }; 569 | 570 | $.fn.ratingLocales.en = { 571 | defaultCaption: '{rating} Stars', 572 | starCaptions: { 573 | 0.5: 'Half Star', 574 | 1: 'One Star', 575 | 1.5: 'One & Half Star', 576 | 2: 'Two Stars', 577 | 2.5: 'Two & Half Stars', 578 | 3: 'Three Stars', 579 | 3.5: 'Three & Half Stars', 580 | 4: 'Four Stars', 581 | 4.5: 'Four & Half Stars', 582 | 5: 'Five Stars' 583 | }, 584 | clearButtonTitle: 'Clear', 585 | clearCaption: 'Not Rated' 586 | }; 587 | 588 | $.fn.rating.Constructor = Rating; 589 | 590 | /** 591 | * Convert automatically inputs with class 'rating' into Krajee's star rating control. 592 | */ 593 | $(document).ready(function () { 594 | var $input = $('input.rating'); 595 | if ($input.length) { 596 | $input.removeClass('rating-loading').addClass('rating-loading').rating(); 597 | } 598 | }); 599 | })); -------------------------------------------------------------------------------- /src/main/resources/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Rate my cat! 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 49 | 50 | 98 | 99 | -------------------------------------------------------------------------------- /src/test/java/io/github/bonigarcia/test/e2e/UserInferfaceTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2017 Boni Garcia (https://bonigarcia.github.io/) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package io.github.bonigarcia.test.e2e; 18 | 19 | import static org.hamcrest.CoreMatchers.containsString; 20 | import static org.hamcrest.CoreMatchers.equalTo; 21 | import static org.hamcrest.MatcherAssert.assertThat; 22 | import static org.openqa.selenium.support.ui.ExpectedConditions.elementToBeClickable; 23 | import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; 24 | 25 | import java.time.Duration; 26 | import java.util.List; 27 | 28 | import org.junit.jupiter.api.DisplayName; 29 | import org.junit.jupiter.api.Tag; 30 | import org.junit.jupiter.api.Test; 31 | import org.junit.jupiter.api.extension.ExtendWith; 32 | import org.openqa.selenium.By; 33 | import org.openqa.selenium.WebElement; 34 | import org.openqa.selenium.chrome.ChromeDriver; 35 | import org.openqa.selenium.firefox.FirefoxDriver; 36 | import org.openqa.selenium.support.ui.WebDriverWait; 37 | import org.springframework.boot.test.context.SpringBootTest; 38 | import org.springframework.boot.test.web.server.LocalServerPort; 39 | import org.springframework.test.context.junit.jupiter.SpringExtension; 40 | 41 | import io.github.bonigarcia.seljup.Arguments; 42 | import io.github.bonigarcia.seljup.SeleniumJupiter; 43 | 44 | @ExtendWith({ SpringExtension.class, SeleniumJupiter.class }) 45 | @SpringBootTest(webEnvironment = RANDOM_PORT) 46 | @DisplayName("E2E tests: user interface") 47 | @Tag("e2e") 48 | class UserInferfaceTest { 49 | 50 | @LocalServerPort 51 | int serverPort; 52 | 53 | @Test 54 | @DisplayName("List cats in the GUI") 55 | @Tag("functional-requirement-1") 56 | void testListCats( 57 | @Arguments("--remote-allow-origins=*") ChromeDriver driver) { 58 | driver.get("http://localhost:" + serverPort); 59 | List catLinks = driver 60 | .findElements(By.className("lightbox")); 61 | assertThat(catLinks.size(), equalTo(9)); 62 | } 63 | 64 | @Test 65 | @DisplayName("Rate a cat using the GUI") 66 | @Tag("functional-requirement-2") 67 | void testRateCat(FirefoxDriver driver) { 68 | driver.get("http://localhost:" + serverPort); 69 | driver.findElement(By.id("Baby")).click(); 70 | 71 | String fourStarsSelector = "#form1 span:nth-child(4)"; 72 | new WebDriverWait(driver, Duration.ofSeconds(10)) 73 | .until(elementToBeClickable(By.cssSelector(fourStarsSelector))); 74 | driver.findElement(By.cssSelector(fourStarsSelector)).click(); 75 | 76 | driver.findElement(By.xpath("//*[@id=\"comment\"]")) 77 | .sendKeys("Very nice cat"); 78 | driver.findElement(By.cssSelector("#form1 > button")).click(); 79 | 80 | WebElement sucessDiv = driver 81 | .findElement(By.cssSelector("#success > div")); 82 | assertThat(sucessDiv.getText(), containsString("Your vote for Baby")); 83 | } 84 | 85 | @Test 86 | @DisplayName("Rate a cat using the GUI with error") 87 | @Tag("functional-requirement-2") 88 | void testRateCatWithError(@Arguments({ "--headless", 89 | "--remote-allow-origins=*" }) ChromeDriver driver) { 90 | driver.get("http://localhost:" + serverPort); 91 | driver.findElement(By.id("Baby")).click(); 92 | 93 | String sendButtonSelector = "#form1 > button"; 94 | new WebDriverWait(driver, Duration.ofSeconds(10)).until( 95 | elementToBeClickable(By.cssSelector(sendButtonSelector))); 96 | driver.findElement(By.cssSelector(sendButtonSelector)).click(); 97 | 98 | WebElement sucessDiv = driver 99 | .findElement(By.cssSelector("#error > div")); 100 | assertThat(sucessDiv.getText(), containsString( 101 | "You need to select some stars for rating each cat")); 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /src/test/java/io/github/bonigarcia/test/integration/WebContextTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2017 Boni Garcia (https://bonigarcia.github.io/) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package io.github.bonigarcia.test.integration; 18 | 19 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 20 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 21 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 22 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 23 | 24 | import org.junit.jupiter.api.DisplayName; 25 | import org.junit.jupiter.api.Tag; 26 | import org.junit.jupiter.api.Test; 27 | import org.junit.jupiter.api.extension.ExtendWith; 28 | import org.springframework.beans.factory.annotation.Autowired; 29 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 30 | import org.springframework.boot.test.context.SpringBootTest; 31 | import org.springframework.test.context.junit.jupiter.SpringExtension; 32 | import org.springframework.test.web.servlet.MockMvc; 33 | 34 | @ExtendWith(SpringExtension.class) 35 | @SpringBootTest 36 | @AutoConfigureMockMvc 37 | @DisplayName("Integration tests: HTTP reponses") 38 | @Tag("integration") 39 | @Tag("functional-requirement-1") 40 | @Tag("functional-requirement-2") 41 | class WebContextTest { 42 | 43 | @Autowired 44 | MockMvc mockMvc; 45 | 46 | @Test 47 | @DisplayName("Check home page (GET /)") 48 | void testHomePage() throws Exception { 49 | mockMvc.perform(get("/")).andExpect(status().isOk()) 50 | .andExpect(content().contentType("text/html;charset=UTF-8")); 51 | } 52 | 53 | @Test 54 | @DisplayName("Check rate cat (POST /)") 55 | void testRatePage() throws Exception { 56 | mockMvc.perform(post("/").param("catId", "1").param("stars", "1") 57 | .param("comment", "")).andExpect(status().isOk()) 58 | .andExpect(content().contentType("text/html;charset=UTF-8")); 59 | } 60 | 61 | @Test 62 | @DisplayName("Check rate cat (POST /) of an non-existing cat") 63 | void testRatePageCatNotAvailable() throws Exception { 64 | mockMvc.perform(post("/").param("catId", "0").param("stars", "1") 65 | .param("comment", "")).andExpect(status().isOk()) 66 | .andExpect(content().contentType("text/html;charset=UTF-8")); 67 | } 68 | 69 | @Test 70 | @DisplayName("Check rate cat (POST /) with bad parameters") 71 | void testRatePageNoParameters() throws Exception { 72 | mockMvc.perform(post("/")).andExpect(status().isBadRequest()); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/test/java/io/github/bonigarcia/test/unit/CookiesTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2017 Boni Garcia (https://bonigarcia.github.io/) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package io.github.bonigarcia.test.unit; 18 | 19 | import static org.hamcrest.CoreMatchers.containsString; 20 | import static org.hamcrest.CoreMatchers.equalTo; 21 | import static org.hamcrest.CoreMatchers.not; 22 | import static org.hamcrest.MatcherAssert.assertThat; 23 | import static org.hamcrest.collection.IsEmptyCollection.empty; 24 | import static org.mockito.ArgumentMatchers.any; 25 | import static org.mockito.Mockito.doNothing; 26 | 27 | import java.util.List; 28 | 29 | import org.junit.jupiter.api.DisplayName; 30 | import org.junit.jupiter.api.Tag; 31 | import org.junit.jupiter.api.Test; 32 | import org.junit.jupiter.api.extension.ExtendWith; 33 | import org.mockito.InjectMocks; 34 | import org.mockito.Mock; 35 | import org.mockito.junit.jupiter.MockitoExtension; 36 | 37 | import io.github.bonigarcia.Cat; 38 | import io.github.bonigarcia.CookiesService; 39 | import io.github.bonigarcia.Opinion; 40 | import jakarta.servlet.http.Cookie; 41 | import jakarta.servlet.http.HttpServletResponse; 42 | 43 | @ExtendWith(MockitoExtension.class) 44 | @DisplayName("Unit tests (white-box): handling cookies") 45 | @Tag("unit") 46 | @Tag("functional-requirement-5") 47 | class CookiesTest { 48 | 49 | @InjectMocks 50 | CookiesService cookiesService; 51 | 52 | @Mock 53 | HttpServletResponse response; 54 | 55 | // Test data 56 | Cat dummy = new Cat("dummy", "dummy.png"); 57 | String dummyCookie = "0#0.0#_"; 58 | 59 | @Test 60 | @DisplayName("Update cookies test") 61 | void testUpdateCookies() { 62 | doNothing().when(response).addCookie(any(Cookie.class)); 63 | String cookies = cookiesService.updateCookies("", 0L, 0D, "", response); 64 | assertThat(cookies, containsString(CookiesService.VALUE_SEPARATOR)); 65 | assertThat(cookies, containsString(CookiesService.CAT_SEPARATOR)); 66 | } 67 | 68 | @Test 69 | @DisplayName("Check cat in cookies") 70 | void testCheckCatInCookies() { 71 | boolean catInCookies = cookiesService.isCatInCookies(dummy, 72 | dummyCookie); 73 | assertThat(catInCookies, equalTo(true)); 74 | } 75 | 76 | @DisplayName("Check cat in empty cookies") 77 | @Test 78 | void testCheckCatInEmptyCookies() { 79 | boolean catInCookies = cookiesService.isCatInCookies(dummy, ""); 80 | assertThat(catInCookies, equalTo(false)); 81 | } 82 | 83 | @DisplayName("Update opinions with cookies") 84 | @Test 85 | void testUpdateOpinionsWithCookies() { 86 | List opinions = cookiesService 87 | .updateOpinionsWithCookiesValue(dummy, dummyCookie); 88 | assertThat(opinions, not(empty())); 89 | } 90 | 91 | @DisplayName("Update opinions with empty cookies") 92 | @Test 93 | void testUpdateOpinionsWithEmptyCookies() { 94 | List opinions = cookiesService 95 | .updateOpinionsWithCookiesValue(dummy, ""); 96 | assertThat(opinions, empty()); 97 | } 98 | 99 | } -------------------------------------------------------------------------------- /src/test/java/io/github/bonigarcia/test/unit/RateCatsTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2017 Boni Garcia (https://bonigarcia.github.io/) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package io.github.bonigarcia.test.unit; 18 | 19 | import static org.hamcrest.CoreMatchers.equalTo; 20 | import static org.hamcrest.MatcherAssert.assertThat; 21 | import static org.hamcrest.text.IsEmptyString.emptyString; 22 | import static org.junit.jupiter.api.Assertions.assertThrows; 23 | import static org.mockito.ArgumentMatchers.any; 24 | import static org.mockito.Mockito.when; 25 | 26 | import java.util.Optional; 27 | 28 | import org.junit.jupiter.api.DisplayName; 29 | import org.junit.jupiter.api.Tag; 30 | import org.junit.jupiter.api.Test; 31 | import org.junit.jupiter.api.extension.ExtendWith; 32 | import org.junit.jupiter.params.ParameterizedTest; 33 | import org.junit.jupiter.params.provider.ValueSource; 34 | import org.mockito.InjectMocks; 35 | import org.mockito.Mock; 36 | import org.mockito.junit.jupiter.MockitoExtension; 37 | 38 | import io.github.bonigarcia.Cat; 39 | import io.github.bonigarcia.CatException; 40 | import io.github.bonigarcia.CatRepository; 41 | import io.github.bonigarcia.CatService; 42 | 43 | @ExtendWith(MockitoExtension.class) 44 | @DisplayName("Unit tests (black-box): rating cats") 45 | @Tag("unit") 46 | class RateCatsTest { 47 | 48 | @InjectMocks 49 | CatService catService; 50 | 51 | @Mock 52 | CatRepository catRepository; 53 | 54 | // Test data 55 | Cat dummy = new Cat("dummy", "dummy.png"); 56 | int stars = 5; 57 | String comment = "foo"; 58 | 59 | @ParameterizedTest(name = "Rating cat with {0} stars") 60 | @ValueSource(doubles = { 0.5, 5 }) 61 | @DisplayName("Correct range of stars test") 62 | @Tag("functional-requirement-3") 63 | void testCorrectRangeOfStars(double stars) { 64 | Cat dummyCat = catService.rateCat(stars, dummy); 65 | assertThat(dummyCat.getAverageRate(), equalTo(stars)); 66 | } 67 | 68 | @ParameterizedTest(name = "Rating cat with {0} stars") 69 | @ValueSource(ints = { 0, 6 }) 70 | @DisplayName("Incorrect range of stars test") 71 | @Tag("functional-requirement-3") 72 | void testIncorrectRangeOfStars(int stars) { 73 | assertThrows(CatException.class, () -> { 74 | catService.rateCat(stars, dummy); 75 | }); 76 | } 77 | 78 | @Test 79 | @DisplayName("Rating cats with a comment") 80 | @Tag("functional-requirement-4") 81 | void testRatingWithComments() { 82 | when(catRepository.findById(any(Long.class))) 83 | .thenReturn(Optional.of(dummy)); 84 | Cat dummyCat = catService.rateCat(stars, comment, 0); 85 | assertThat( 86 | catService.getOpinions(dummyCat).iterator().next().getComment(), 87 | equalTo(comment)); 88 | } 89 | 90 | @Test 91 | @DisplayName("Rating cats with empty comment") 92 | @Tag("functional-requirement-4") 93 | void testRatingWithEmptyComments() { 94 | when(catRepository.findById(any(Long.class))) 95 | .thenReturn(Optional.of(dummy)); 96 | Cat dummyCat = catService.rateCat(stars, dummy); 97 | assertThat( 98 | catService.getOpinions(dummyCat).iterator().next().getComment(), 99 | emptyString()); 100 | } 101 | 102 | } 103 | --------------------------------------------------------------------------------