├── .editorconfig ├── .eslintignore ├── .gitattributes ├── .github └── workflows │ └── maven.yml ├── .gitignore ├── .mvn ├── jvm.config └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── .sonarcloud.properties ├── Dockerfile ├── README.md ├── docker-compose.yml ├── docker-steps.md ├── mvnw ├── mvnw.cmd ├── pom.xml ├── screenshots ├── logged-in-feed-view.png ├── logged-in-note-list-view.png └── public-view.png ├── spot-bugs.filter-exclude.xml └── src ├── main ├── java │ └── gt │ │ └── app │ │ ├── Application.java │ │ ├── DataCreator.java │ │ ├── config │ │ ├── AppProperties.java │ │ ├── AuditorResolver.java │ │ ├── Constants.java │ │ ├── JpaConfig.java │ │ └── security │ │ │ ├── AppUserDetails.java │ │ │ ├── SecurityConfig.java │ │ │ └── SecurityUtils.java │ │ ├── domain │ │ ├── AppUser.java │ │ ├── Authority.java │ │ ├── BaseAuditingEntity.java │ │ ├── BaseEntity.java │ │ ├── LiteUser.java │ │ ├── Note.java │ │ └── ReceivedFile.java │ │ ├── exception │ │ ├── InvalidDataException.java │ │ ├── OperationNotAllowedException.java │ │ └── RecordNotFoundException.java │ │ ├── modules │ │ ├── email │ │ │ ├── EmailService.java │ │ │ ├── EmailUtil.java │ │ │ └── dto │ │ │ │ └── EmailDto.java │ │ ├── file │ │ │ ├── FileDownloadUtil.java │ │ │ ├── FileService.java │ │ │ ├── ReceivedFileRepository.java │ │ │ ├── ReceivedFileService.java │ │ │ ├── RetrievalException.java │ │ │ └── StorageException.java │ │ ├── note │ │ │ ├── NoteRepository.java │ │ │ ├── NoteService.java │ │ │ └── dto │ │ │ │ ├── NoteCreateDto.java │ │ │ │ ├── NoteEditDto.java │ │ │ │ ├── NoteMapper.java │ │ │ │ └── NoteReadDto.java │ │ └── user │ │ │ ├── AppPermissionEvaluatorService.java │ │ │ ├── AppUserDetailsService.java │ │ │ ├── AuthorityRepository.java │ │ │ ├── AuthorityService.java │ │ │ ├── LiteUserRepository.java │ │ │ ├── PasswordUpdateValidator.java │ │ │ ├── UserAuthorityService.java │ │ │ ├── UserRepository.java │ │ │ ├── UserService.java │ │ │ ├── UserSignupValidator.java │ │ │ └── dto │ │ │ ├── PasswordUpdateDTO.java │ │ │ ├── UserDTO.java │ │ │ ├── UserMapper.java │ │ │ ├── UserProfileUpdateDTO.java │ │ │ └── UserSignUpDTO.java │ │ └── web │ │ ├── mvc │ │ ├── DownloadController.java │ │ ├── ErrorControllerAdvice.java │ │ ├── IndexController.java │ │ ├── NoteController.java │ │ └── UserController.java │ │ └── rest │ │ ├── HelloResource.java │ │ └── UserResource.java └── resources │ ├── application-default.yml │ ├── application-dev.yml │ ├── application-docker.yml │ ├── application-prod.yml │ ├── application.yml │ ├── checkstyle.xml │ ├── static │ ├── css │ │ ├── app.css │ │ └── app2.css │ ├── img │ │ ├── male-coat.png │ │ └── male-tshirt.png │ └── js │ │ ├── app.js │ │ └── custom.js │ ├── templates │ ├── _fragments │ │ ├── footer.html │ │ └── header.html │ ├── admin.html │ ├── error.html │ ├── landing.html │ ├── note.html │ ├── note │ │ ├── _notes.html │ │ └── edit-note.html │ └── user │ │ ├── password.html │ │ ├── profile.html │ │ └── signup.html │ ├── wro.properties │ └── wro.xml └── test ├── groovy └── gt │ └── app │ ├── DataDrivenSpec.groovy │ ├── SpockExSpec.groovy │ ├── SpringContextSpec.groovy │ └── modules │ └── user │ └── AppUserServiceSpec.groovy ├── java └── gt │ └── app │ ├── ApplicationStartupTest.java │ ├── ApplicationTest.java │ ├── arch │ ├── ArchitectureTest.java │ ├── GeneralCodingRulesTest.java │ └── SpringCodingRulesTest.java │ ├── config │ ├── DBMetadataReader.java │ ├── HibernateConfig.java │ └── MetadataExtractorIntegrator.java │ ├── e2e │ ├── WebAppIT.java │ └── pageobj │ │ ├── BaseLoggedInPage.java │ │ ├── BasePage.java │ │ ├── LoggedInHomePage.java │ │ ├── LoginPage.java │ │ ├── NoteEditPage.java │ │ ├── PublicPage.java │ │ └── UserPage.java │ ├── frwk │ ├── BaseSeleniumTest.java │ ├── SampleTest.java │ ├── TestDataManager.java │ └── TestUtil.java │ ├── modules │ └── file │ │ └── FileDownloadUtilTest.java │ └── web │ └── rest │ ├── AppUserResourceIT.java │ └── HelloResourceIT.java └── resources ├── application-test.yml ├── archunit.properties ├── blob └── test.txt └── logback.xml /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 4 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | 22 | [{package,bower}.json] 23 | indent_style = space 24 | indent_size = 2 25 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # more info here - http://eslint.org/docs/user-guide/configuring.html#ignoring-files-and-directories 2 | 3 | # node_modules ignored by default 4 | 5 | # ignore bower_components 6 | src/main/webapp/bower_components 7 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # All text files should have the "lf" (Unix) line endings 2 | * text eol=lf 3 | 4 | # Explicitly declare text files you want to always be normalized and converted 5 | # to native line endings on checkout. 6 | *.java text 7 | *.js text 8 | *.css text 9 | *.html text 10 | 11 | # Denote all files that are truly binary and should not be modified. 12 | *.png binary 13 | *.jpg binary 14 | *.jar binary 15 | *.pdf binary 16 | *.eot binary 17 | *.ttf binary 18 | *.gzip binary 19 | *.gz binary 20 | *.ai binary 21 | *.eps binary 22 | *.swf binary 23 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: Java CI with Maven 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - uses: browser-actions/setup-edge@v1 20 | 21 | - name: Set up JDK 21 22 | uses: actions/setup-java@v1 23 | with: 24 | java-version: 21 25 | cache: 'maven' 26 | - name: Build with Maven 27 | run: mvn -B verify --file pom.xml 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | .sts4-cache 12 | 13 | ### IntelliJ IDEA ### 14 | .idea 15 | *.iws 16 | *.iml 17 | *.ipr 18 | 19 | ### NetBeans ### 20 | nbproject/private/ 21 | build/ 22 | nbbuild/ 23 | dist/ 24 | nbdist/ 25 | .nb-gradle/ 26 | 27 | 28 | ### VS Code ### 29 | .vscode/** 30 | 31 | ###################### 32 | # Maven 33 | ###################### 34 | /log/ 35 | /target/ 36 | 37 | 38 | 39 | ###################### 40 | # Logs 41 | ###################### 42 | *.log* 43 | -------------------------------------------------------------------------------- /.mvn/jvm.config: -------------------------------------------------------------------------------- 1 | --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED 2 | --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED 3 | --add-exports jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED 4 | --add-exports jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED 5 | --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED 6 | --add-exports jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED 7 | --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED 8 | --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED 9 | --add-opens jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED 10 | --add-opens jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED 11 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gtiwari333/spring-boot-blog-app/aa3d93f6e8e22b52bc7506a89de73bbbd5fdd26f/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.5/apache-maven-3.9.5-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar 3 | -------------------------------------------------------------------------------- /.sonarcloud.properties: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ## this is conventional process 2 | FROM openjdk:21-slim 3 | VOLUME /tmp 4 | VOLUME /X/attachments 5 | COPY target/*.jar app.jar 6 | ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"] 7 | 8 | 9 | ## spring boot supports optimized/layed docker image generation support 10 | # use :mvn package spring-boot:build-image 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A BlogApp with Spring Boot 2 | 3 | #### More complex version is here: https://github.com/gtiwari333/spring-boot-web-application-seed 4 | 5 | ### Intro 6 | 7 | This is a simple micro blogging application where you can post a note/blog with attachments and other can view it. 8 | 9 | The default username/passwords are listed on : gt.app.Application.initData, which are: 10 | 11 | - system/pass 12 | - user1/pass 13 | - user2/pass 14 | 15 | ### Requirements 16 | 17 | - JDK 21+ 18 | - Lombok configured on IDE 19 | - http://ganeshtiwaridotcomdotnp.blogspot.com/2016/03/configuring-lombok-on-intellij.html 20 | - For eclipse, download the lombok jar, run it, and point to eclipse installation 21 | - Maven (optional) 22 | - Docker 23 | 24 | ### How to Run 25 | 26 | - Clone/Download and Import project into your IDE, compile and run Application.java 27 | - Update run configuration to run maven goal `wro4j:run` Before Launch. It should be after 'Build' 28 | OR 29 | 30 | - ./mvnw compile spring-boot:run //if you don't have maven installed in your PC 31 | 32 | OR 33 | 34 | - ./mvnw compile spring-boot:run //if you have maven installed in your PC 35 | 36 | And open `http://localhost:8080` on your browser 37 | 38 | Optionally, you can start the docker containers yourself using: 39 | 40 | `docker-compose --profile mailHog up` to start just the mailHog container(required by default 'dev' profile) 41 | 42 | Or 43 | 44 | `docker-compose --profile all up` to start both mailHog and mysql (if you want to use 'docker' or 'prod' profile) 45 | 46 | `sudo chmod 666 /var/run/docker.sock` to fix following error 47 | ``` 48 | org.springframework.boot.docker.compose.core.ProcessExitException: 'docker version --format {{.Client.Version}}' failed with exit code 1. 49 | 50 | Stdout: 51 | 20.10.24 52 | 53 | 54 | Stderr: 55 | Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: 56 | Get "http://%2Fvar%2Frun%2Fdocker.sock/v1.24/version": dial unix /var/run/docker.sock: connect: permission denied 57 | ``` 58 | 59 | ## Run Tests 60 | 61 | ##### Running full tests 62 | 63 | `./mvnw clean verify` 64 | 65 | ##### Running unit tests only (it uses maven surefire plugin) 66 | 67 | `./mvnw compiler:testCompile resources:testResources surefire:test` 68 | 69 | ##### Running integration tests only (it uses maven-failsafe-plugin) 70 | 71 | `./mvnw compiler:testCompile resources:testResources failsafe:integration-test` 72 | 73 | ## Code Quality 74 | 75 | ##### The `error-prone` runs at compile time. 76 | 77 | ##### The `modernizer` `checkstyle` and `spotbugs` plugin are run as part of maven `test-compile` lifecycle phase. use `mvn spotbugs:gui' to 78 | 79 | ##### SonarQube scan 80 | 81 | Run sonarqube server using docker 82 | `docker run -e SONAR_ES_BOOTSTRAP_CHECKS_DISABLE=true -p 9000:9000 sonarqube:latest` 83 | 84 | Perform scan: 85 | `./mvnw sonar:sonar` 86 | ./mvnw sonar:sonar -Dsonar.login=admin -Dsonar.password=admin 87 | 88 | View Reports in SonarQube web ui: 89 | 90 | - visit http://localhost:9000 91 | - default login and password are `admin`, you will be asked to change password after logging in with default 92 | username/password 93 | - (optional) change sonarqube admin password without logging 94 | in: `curl -u admin:admin -X POST "http://localhost:9000/api/users/change_password?login=admin&previousPassword=admin&password=NEW_PASSWORD"` 95 | - if you change the password, make sure the update `-Dsonar.password=admin` when you run sonarqube next time 96 | 97 | ### Dependency vulnerability scan 98 | 99 | Owasp dependency check plugin is configured. Run `./mvnw dependency-check:check` to run scan and 100 | open `dependency-check-report.html` from target to see the report. 101 | 102 | ### Dependency/plugin version checker 103 | 104 | ./mvnw versions:display-dependency-updates 105 | ./mvnw versions:display-plugin-updates 106 | 107 | ### Included Features/Samples 108 | - GraalVM native image generation 109 | - Modular application 110 | - Data JPA with User/Authority/Note/ReceivedFile entities, example of EntityGraph 111 | - Default test data created while running the app 112 | - Public and internal pages 113 | - MVC with thymeleaf templating 114 | - File upload/download 115 | - Live update of thymeleaf templates for local development 116 | - HTML fragments 117 | - webjar - bootstrap4 + jquery 118 | - Custom Error page 119 | - Request logger filter 120 | - Swagger API Docs with UI ( http://localhost:8080/swagger-ui.html) 121 | - @RestControllerAdvice, @ControllerAdvice demo 122 | - CRUD Note + File upload 123 | - Spring / Maven profiles for dev/prod ... 124 | - Dockerfile to run images 125 | - Docker maven plugin to publish images (follow docker-steps.md) 126 | - Deploy to Amazon EC2 ( follow docker-steps.md ) 127 | - Code Generation: lombok, mapstruct 128 | - H2 db for local, Console enabled for local ( http://localhost:8080/h2-console/, db url: jdbc:h2:mem:testdb, username:sa) 129 | - MySQL or any other SQL db can be configured for prod/docker etc profiles 130 | - User/User_Authority entity and repository/services 131 | - login, logout, home pages based on user role 132 | - Security with basic config 133 | - Domain object Access security check on update/delete using custom PermissionEvaluator 134 | - public home page -- view all notes by all 135 | - private pages based on user roles 136 | - Test cases - unit/integration with JUnit 5, Mockito and Spring Test 137 | - Tests with Spock Framework (Groovy 3, Spock 2) 138 | - e2e with Selenide, fixtures. default data generated using Spring 139 | - Architecture test using ArchUnit 140 | - Email 141 | - Account management/Signup UI 142 | 143 | Future: do more stuff 144 | 145 | - background jobs with Quartz 146 | - Liquibase/Flyway change log 147 | - Integrate Markdown editor for writing notes 148 | 149 | ### Dependency/plugin version checker 150 | 151 | `./mvnw versions:display-dependency-updates` 152 | `./mvnw versions:display-plugin-updates` 153 | 154 | ## Create docker image using buildpack 155 | 156 | ./mvnw spring-boot:build-image 157 | 158 | docker run --rm -p 8080:8080 docker.io/library/note-app:3.2.1 159 | 160 | 161 | ## Generate native executable: 162 | - Required: GraalVM 22.3+ (for Spring Boot 3) 163 | - Install using sdkman 164 | - https://docs.spring.io/spring-boot/docs/current/reference/html/native-image.html#native-image.developing-your-first-application.native-build-tools.prerequisites 165 | `sdk install java 22.3.r17-nik` 166 | `sdk use java 22.3.r17-nik` 167 | 168 | - Create native executable `./mvnw native:compile -Pnative,dev` 169 | - Run it `./target/note-app` 170 | 171 | OR 172 | 173 | - Generate docker image with native executable `./mvnw spring-boot:build-image -Pnative,dev` 174 | - Run it `docker run --rm -p 8080:8080 docker.io/library/note-app:3.2.1` 175 | 176 | 177 | ## Native Test: 178 | - Run with `./mvnw test -PnativeTest` 179 | - Spring Boot 3.0.0: native-test is not working due to spock ( and possibly other dependencies too) 180 | 181 | 182 | 183 | # Results after enabling virtual thread 184 | 185 | ab -k -c 10 -n 2000 http://localhost:8080/ 186 | 187 | 188 | 189 | Before 190 | ``` 191 | Connection Times (ms) 192 | min mean[+/-sd] median max 193 | Connect: 0 0 0.1 0 1 194 | Processing: 3 4 0.9 4 9 195 | Waiting: 3 4 0.8 4 8 196 | Total: 3 4 0.9 4 9 197 | 198 | Percentage of the requests served within a certain time (ms) 199 | 50% 4 200 | 66% 4 201 | 75% 5 202 | 80% 5 203 | 90% 5 204 | 95% 6 205 | 98% 7 206 | 99% 8 207 | 100% 9 (longest request) 208 | 209 | ``` 210 | 211 | 212 | After 213 | ``` 214 | Connection Times (ms) 215 | min mean[+/-sd] median max 216 | Connect: 0 0 0.1 0 1 217 | Processing: 3 4 0.7 4 9 218 | Waiting: 3 4 0.7 4 8 219 | Total: 3 5 0.7 4 9 220 | WARNING: The median and mean for the total time are not within a normal deviation 221 | These results are probably not that reliable. 222 | 223 | Percentage of the requests served within a certain time (ms) 224 | 50% 4 225 | 66% 5 226 | 75% 5 227 | 80% 5 228 | 90% 5 229 | 95% 6 230 | 98% 6 231 | 99% 7 232 | 100% 9 (longest request) 233 | 234 | ``` 235 | 236 | 237 | After introducing a delay to simulate slow blocking API and thousand concurrent requests. Its similar for less concurrent request. Virtual thread outperforms when we have too many concurrent requests. 238 | 239 | 240 | ab -c 1000 -n 15000 http://localhost:8080/ 241 | 242 | ```java 243 | public class IndexController { 244 | 245 | @GetMapping({"/", ""}) 246 | public String index(Model model, Pageable pageable) throws InterruptedException { 247 | Thread.sleep(1500); 248 | ... 249 | } 250 | ``` 251 | 252 | before 253 | 254 | ``` 255 | Connection Times (ms) 256 | min mean[+/-sd] median max 257 | Connect: 0 19 132.4 0 1030 258 | Processing: 1529 7341 918.6 7547 7631 259 | Waiting: 1528 7340 918.7 7547 7630 260 | Total: 1555 7359 905.6 7548 7817 261 | 262 | Percentage of the requests served within a certain time (ms) 263 | 50% 7548 264 | 66% 7552 265 | 75% 7556 266 | 80% 7558 267 | 90% 7570 268 | 95% 7585 269 | 98% 7611 270 | 99% 7628 271 | 100% 7817 (longest request) 272 | 273 | ``` 274 | 275 | after 276 | 277 | AMAZING !!! 278 | 279 | 280 | ``` 281 | 282 | Connection Times (ms) 283 | min mean[+/-sd] median max 284 | Connect: 0 1 4.7 0 23 285 | Processing: 1503 1526 60.8 1507 1919 286 | Waiting: 1503 1526 60.8 1506 1918 287 | Total: 1503 1528 64.4 1507 1940 288 | 289 | Percentage of the requests served within a certain time (ms) 290 | 50% 1507 291 | 66% 1510 292 | 75% 1514 293 | 80% 1519 294 | 90% 1559 295 | 95% 1664 296 | 98% 1804 297 | 99% 1867 298 | 100% 1940 (longest request) 299 | 300 | 301 | ``` 302 | 303 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # spring boot initializes this docker-compose (with local profile) if `spring.docker.compose.profiles.active=local` property is set 2 | version: '3' 3 | services: 4 | emailhog: 5 | image: 'mailhog/mailhog' 6 | container_name: mailhog 7 | ports: 8 | - 1025:1025 9 | networks: 10 | - note-app-network 11 | profiles: 12 | - mailHog 13 | - all 14 | mysql: 15 | image: 'mysql' 16 | environment: 17 | - "MYSQL_ROOT_PASSWORD=password" 18 | - "MYSQL_DATABASE=noteappdb" 19 | ports: 20 | - 3306:3306 21 | networks: 22 | - note-app-network 23 | labels: 24 | #org.springframework.boot.ignore: true #use this to omit this from initialization 25 | # this will be sent to org.springframework.boot.docker.compose.service.connection.jdbc.JdbcUrlBuilder.build to build jdbc URL 26 | org.springframework.boot.jdbc.parameters: useUnicode=true&characterEncoding=utf8&allowPublicKeyRetrieval=true&useSSL=false 27 | profiles: 28 | - all 29 | 30 | networks: 31 | note-app-network: 32 | driver: bridge 33 | 34 | 35 | #run using 36 | #docker-compose --profile all up OR 37 | #docker-compose --profile mailHog up 38 | 39 | -------------------------------------------------------------------------------- /docker-steps.md: -------------------------------------------------------------------------------- 1 | Run locally 2 | === 3 | mvn clean compile package -Pdocker 4 | sudo docker build -t app . 5 | sudo docker run -p 8080:8080 app 6 | 7 | 8 | Run on AWS EC2: 9 | === 10 | 11 | Referene: https://docs.aws.amazon.com/AmazonECR/latest/userguide/docker-basics.html 12 | 13 | ### 1. Install AWS CLI: 14 | $sudo apt install awscli 15 | 16 | ### 2. AWS configure: 17 | Follow https://docs.aws.amazon.com/IAM/latest/UserGuide/getting-started_create-admin-group.html for access key id and secret to create IAM user 18 | $aws configure >> asks you to enter id and secret key created earlier ( can download csv ) 19 | 20 | ### 3. Create repo 21 | $aws ecr create-repository --repository-name MY_REPOSITORY --region REGION 22 | 23 | The above returns Output: note repositorUri 24 | { 25 | "repository": { 26 | "repositoryArn": "arn:aws:ecr:us-east-1:159931644654:repository/noteapp", 27 | "registryId": "159931644654", 28 | "repositoryName": "noteapp", 29 | "repositoryUri": "159931644654.dkr.ecr.us-east-1.amazonaws.com/noteapp", 30 | "createdAt": 1565552020.0, 31 | "imageTagMutability": "MUTABLE" 32 | } 33 | } 34 | 35 | ### 4. Build image and tag 36 | $docker tag MY_DOCKER_APP_IMAGE repositoryUri << repositoryUri from above output 37 | 38 | ### 5. Login to ECR docker 39 | $aws ecr get-login --no-include-email --region region << returns a command, run it 40 | 41 | ### 6. Push to repo 42 | $docker push repositoryUri << repositoryUri from above output 43 | 44 | ### 7. View ECR Repository to verify, note the region on url 45 | https://us-east-1.console.aws.amazon.com/ecr/repositories?region=us-east-1 46 | 47 | Example 48 | === 49 | 50 | $ aws configure 51 | $ sudo aws ecr create-repository --repository-name noteapp --region us-east-1 << returned repositoryUri": "159931644654.dkr.ecr.us-east-1.amazonaws.com/noteapp 52 | $ mvn clean compile package -Pdocker 53 | $ sudo docker build -t app . 54 | $ sudo docker tag app 159931644654.dkr.ecr.us-east-1.amazonaws.com/noteapp 55 | $ aws ecr get-login --no-include-email --region us-east-1 << otherwise we get error: ``denied: The security token included in the request is invalid.`` 56 | $ sudo docker push 159931644654.dkr.ecr.us-east-1.amazonaws.com/noteapp 57 | 58 | ### Create and Login to EC2 instance 59 | https://docs.aws.amazon.com/AmazonECS/latest/developerguide/get-set-up-for-amazon-ecs.html 60 | 61 | 62 | Terms: 63 | 64 | - EC2 - alternative of beanstalk -- allows to create a machine and clusters 65 | - ECR - container registry -- we can push docker images 66 | - IAM - identity mgmt --> need to create a second account before creating EC2 instances/ publish image on ECR etc .. give full access 67 | 68 | 69 | - Need to create EC2 cluster to deploy docker image 70 | - Create EC2 cluster, a service, task using the docker image and run the task to deploy the image 71 | - A task should be created to do the deploy 72 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Apache Maven Wrapper startup batch script, version 3.2.0 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | # e.g. to debug Maven itself, use 32 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | # ---------------------------------------------------------------------------- 35 | 36 | if [ -z "$MAVEN_SKIP_RC" ] ; then 37 | 38 | if [ -f /usr/local/etc/mavenrc ] ; then 39 | . /usr/local/etc/mavenrc 40 | fi 41 | 42 | if [ -f /etc/mavenrc ] ; then 43 | . /etc/mavenrc 44 | fi 45 | 46 | if [ -f "$HOME/.mavenrc" ] ; then 47 | . "$HOME/.mavenrc" 48 | fi 49 | 50 | fi 51 | 52 | # OS specific support. $var _must_ be set to either true or false. 53 | cygwin=false; 54 | darwin=false; 55 | mingw=false 56 | case "$(uname)" in 57 | CYGWIN*) cygwin=true ;; 58 | MINGW*) mingw=true;; 59 | Darwin*) darwin=true 60 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 61 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 62 | if [ -z "$JAVA_HOME" ]; then 63 | if [ -x "/usr/libexec/java_home" ]; then 64 | JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME 65 | else 66 | JAVA_HOME="/Library/Java/Home"; export JAVA_HOME 67 | fi 68 | fi 69 | ;; 70 | esac 71 | 72 | if [ -z "$JAVA_HOME" ] ; then 73 | if [ -r /etc/gentoo-release ] ; then 74 | JAVA_HOME=$(java-config --jre-home) 75 | fi 76 | fi 77 | 78 | # For Cygwin, ensure paths are in UNIX format before anything is touched 79 | if $cygwin ; then 80 | [ -n "$JAVA_HOME" ] && 81 | JAVA_HOME=$(cygpath --unix "$JAVA_HOME") 82 | [ -n "$CLASSPATH" ] && 83 | CLASSPATH=$(cygpath --path --unix "$CLASSPATH") 84 | fi 85 | 86 | # For Mingw, ensure paths are in UNIX format before anything is touched 87 | if $mingw ; then 88 | [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && 89 | JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" 90 | fi 91 | 92 | if [ -z "$JAVA_HOME" ]; then 93 | javaExecutable="$(which javac)" 94 | if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then 95 | # readlink(1) is not available as standard on Solaris 10. 96 | readLink=$(which readlink) 97 | if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then 98 | if $darwin ; then 99 | javaHome="$(dirname "\"$javaExecutable\"")" 100 | javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" 101 | else 102 | javaExecutable="$(readlink -f "\"$javaExecutable\"")" 103 | fi 104 | javaHome="$(dirname "\"$javaExecutable\"")" 105 | javaHome=$(expr "$javaHome" : '\(.*\)/bin') 106 | JAVA_HOME="$javaHome" 107 | export JAVA_HOME 108 | fi 109 | fi 110 | fi 111 | 112 | if [ -z "$JAVACMD" ] ; then 113 | if [ -n "$JAVA_HOME" ] ; then 114 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 115 | # IBM's JDK on AIX uses strange locations for the executables 116 | JAVACMD="$JAVA_HOME/jre/sh/java" 117 | else 118 | JAVACMD="$JAVA_HOME/bin/java" 119 | fi 120 | else 121 | JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" 122 | fi 123 | fi 124 | 125 | if [ ! -x "$JAVACMD" ] ; then 126 | echo "Error: JAVA_HOME is not defined correctly." >&2 127 | echo " We cannot execute $JAVACMD" >&2 128 | exit 1 129 | fi 130 | 131 | if [ -z "$JAVA_HOME" ] ; then 132 | echo "Warning: JAVA_HOME environment variable is not set." 133 | fi 134 | 135 | # traverses directory structure from process work directory to filesystem root 136 | # first directory with .mvn subdirectory is considered project base directory 137 | find_maven_basedir() { 138 | if [ -z "$1" ] 139 | then 140 | echo "Path not specified to find_maven_basedir" 141 | return 1 142 | fi 143 | 144 | basedir="$1" 145 | wdir="$1" 146 | while [ "$wdir" != '/' ] ; do 147 | if [ -d "$wdir"/.mvn ] ; then 148 | basedir=$wdir 149 | break 150 | fi 151 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 152 | if [ -d "${wdir}" ]; then 153 | wdir=$(cd "$wdir/.." || exit 1; pwd) 154 | fi 155 | # end of workaround 156 | done 157 | printf '%s' "$(cd "$basedir" || exit 1; pwd)" 158 | } 159 | 160 | # concatenates all lines of a file 161 | concat_lines() { 162 | if [ -f "$1" ]; then 163 | # Remove \r in case we run on Windows within Git Bash 164 | # and check out the repository with auto CRLF management 165 | # enabled. Otherwise, we may read lines that are delimited with 166 | # \r\n and produce $'-Xarg\r' rather than -Xarg due to word 167 | # splitting rules. 168 | tr -s '\r\n' ' ' < "$1" 169 | fi 170 | } 171 | 172 | log() { 173 | if [ "$MVNW_VERBOSE" = true ]; then 174 | printf '%s\n' "$1" 175 | fi 176 | } 177 | 178 | BASE_DIR=$(find_maven_basedir "$(dirname "$0")") 179 | if [ -z "$BASE_DIR" ]; then 180 | exit 1; 181 | fi 182 | 183 | MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR 184 | log "$MAVEN_PROJECTBASEDIR" 185 | 186 | ########################################################################################## 187 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 188 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 189 | ########################################################################################## 190 | wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" 191 | if [ -r "$wrapperJarPath" ]; then 192 | log "Found $wrapperJarPath" 193 | else 194 | log "Couldn't find $wrapperJarPath, downloading it ..." 195 | 196 | if [ -n "$MVNW_REPOURL" ]; then 197 | wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" 198 | else 199 | wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" 200 | fi 201 | while IFS="=" read -r key value; do 202 | # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) 203 | safeValue=$(echo "$value" | tr -d '\r') 204 | case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; 205 | esac 206 | done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" 207 | log "Downloading from: $wrapperUrl" 208 | 209 | if $cygwin; then 210 | wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") 211 | fi 212 | 213 | if command -v wget > /dev/null; then 214 | log "Found wget ... using wget" 215 | [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" 216 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 217 | wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 218 | else 219 | wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 220 | fi 221 | elif command -v curl > /dev/null; then 222 | log "Found curl ... using curl" 223 | [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" 224 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 225 | curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" 226 | else 227 | curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" 228 | fi 229 | else 230 | log "Falling back to using Java to download" 231 | javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" 232 | javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" 233 | # For Cygwin, switch paths to Windows format before running javac 234 | if $cygwin; then 235 | javaSource=$(cygpath --path --windows "$javaSource") 236 | javaClass=$(cygpath --path --windows "$javaClass") 237 | fi 238 | if [ -e "$javaSource" ]; then 239 | if [ ! -e "$javaClass" ]; then 240 | log " - Compiling MavenWrapperDownloader.java ..." 241 | ("$JAVA_HOME/bin/javac" "$javaSource") 242 | fi 243 | if [ -e "$javaClass" ]; then 244 | log " - Running MavenWrapperDownloader.java ..." 245 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" 246 | fi 247 | fi 248 | fi 249 | fi 250 | ########################################################################################## 251 | # End of extension 252 | ########################################################################################## 253 | 254 | # If specified, validate the SHA-256 sum of the Maven wrapper jar file 255 | wrapperSha256Sum="" 256 | while IFS="=" read -r key value; do 257 | case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; 258 | esac 259 | done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" 260 | if [ -n "$wrapperSha256Sum" ]; then 261 | wrapperSha256Result=false 262 | if command -v sha256sum > /dev/null; then 263 | if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then 264 | wrapperSha256Result=true 265 | fi 266 | elif command -v shasum > /dev/null; then 267 | if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then 268 | wrapperSha256Result=true 269 | fi 270 | else 271 | echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." 272 | echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." 273 | exit 1 274 | fi 275 | if [ $wrapperSha256Result = false ]; then 276 | echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 277 | echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 278 | echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 279 | exit 1 280 | fi 281 | fi 282 | 283 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 284 | 285 | # For Cygwin, switch paths to Windows format before running java 286 | if $cygwin; then 287 | [ -n "$JAVA_HOME" ] && 288 | JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") 289 | [ -n "$CLASSPATH" ] && 290 | CLASSPATH=$(cygpath --path --windows "$CLASSPATH") 291 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 292 | MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") 293 | fi 294 | 295 | # Provide a "standardized" way to retrieve the CLI args that will 296 | # work with both Windows and non-Windows executions. 297 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" 298 | export MAVEN_CMD_LINE_ARGS 299 | 300 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 301 | 302 | # shellcheck disable=SC2086 # safe args 303 | exec "$JAVACMD" \ 304 | $MAVEN_OPTS \ 305 | $MAVEN_DEBUG_OPTS \ 306 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 307 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 308 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 309 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Apache Maven Wrapper startup batch script, version 3.2.0 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 28 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 29 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 30 | @REM e.g. to debug Maven itself, use 31 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 32 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 33 | @REM ---------------------------------------------------------------------------- 34 | 35 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 36 | @echo off 37 | @REM set title of command window 38 | title %0 39 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 40 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 41 | 42 | @REM set %HOME% to equivalent of $HOME 43 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 44 | 45 | @REM Execute a user defined script before this one 46 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 47 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 48 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* 49 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* 50 | :skipRcPre 51 | 52 | @setlocal 53 | 54 | set ERROR_CODE=0 55 | 56 | @REM To isolate internal variables from possible post scripts, we use another setlocal 57 | @setlocal 58 | 59 | @REM ==== START VALIDATION ==== 60 | if not "%JAVA_HOME%" == "" goto OkJHome 61 | 62 | echo. 63 | echo Error: JAVA_HOME not found in your environment. >&2 64 | echo Please set the JAVA_HOME variable in your environment to match the >&2 65 | echo location of your Java installation. >&2 66 | echo. 67 | goto error 68 | 69 | :OkJHome 70 | if exist "%JAVA_HOME%\bin\java.exe" goto init 71 | 72 | echo. 73 | echo Error: JAVA_HOME is set to an invalid directory. >&2 74 | echo JAVA_HOME = "%JAVA_HOME%" >&2 75 | echo Please set the JAVA_HOME variable in your environment to match the >&2 76 | echo location of your Java installation. >&2 77 | echo. 78 | goto error 79 | 80 | @REM ==== END VALIDATION ==== 81 | 82 | :init 83 | 84 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 85 | @REM Fallback to current working directory if not found. 86 | 87 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 88 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 89 | 90 | set EXEC_DIR=%CD% 91 | set WDIR=%EXEC_DIR% 92 | :findBaseDir 93 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 94 | cd .. 95 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 96 | set WDIR=%CD% 97 | goto findBaseDir 98 | 99 | :baseDirFound 100 | set MAVEN_PROJECTBASEDIR=%WDIR% 101 | cd "%EXEC_DIR%" 102 | goto endDetectBaseDir 103 | 104 | :baseDirNotFound 105 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 106 | cd "%EXEC_DIR%" 107 | 108 | :endDetectBaseDir 109 | 110 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 111 | 112 | @setlocal EnableExtensions EnableDelayedExpansion 113 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 114 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 115 | 116 | :endReadAdditionalConfig 117 | 118 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 119 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 120 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 121 | 122 | set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" 123 | 124 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 125 | IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B 126 | ) 127 | 128 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 129 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 130 | if exist %WRAPPER_JAR% ( 131 | if "%MVNW_VERBOSE%" == "true" ( 132 | echo Found %WRAPPER_JAR% 133 | ) 134 | ) else ( 135 | if not "%MVNW_REPOURL%" == "" ( 136 | SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" 137 | ) 138 | if "%MVNW_VERBOSE%" == "true" ( 139 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 140 | echo Downloading from: %WRAPPER_URL% 141 | ) 142 | 143 | powershell -Command "&{"^ 144 | "$webclient = new-object System.Net.WebClient;"^ 145 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 146 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 147 | "}"^ 148 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ 149 | "}" 150 | if "%MVNW_VERBOSE%" == "true" ( 151 | echo Finished downloading %WRAPPER_JAR% 152 | ) 153 | ) 154 | @REM End of extension 155 | 156 | @REM If specified, validate the SHA-256 sum of the Maven wrapper jar file 157 | SET WRAPPER_SHA_256_SUM="" 158 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 159 | IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B 160 | ) 161 | IF NOT %WRAPPER_SHA_256_SUM%=="" ( 162 | powershell -Command "&{"^ 163 | "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ 164 | "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ 165 | " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ 166 | " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ 167 | " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ 168 | " exit 1;"^ 169 | "}"^ 170 | "}" 171 | if ERRORLEVEL 1 goto error 172 | ) 173 | 174 | @REM Provide a "standardized" way to retrieve the CLI args that will 175 | @REM work with both Windows and non-Windows executions. 176 | set MAVEN_CMD_LINE_ARGS=%* 177 | 178 | %MAVEN_JAVA_EXE% ^ 179 | %JVM_CONFIG_MAVEN_PROPS% ^ 180 | %MAVEN_OPTS% ^ 181 | %MAVEN_DEBUG_OPTS% ^ 182 | -classpath %WRAPPER_JAR% ^ 183 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ 184 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 185 | if ERRORLEVEL 1 goto error 186 | goto end 187 | 188 | :error 189 | set ERROR_CODE=1 190 | 191 | :end 192 | @endlocal & set ERROR_CODE=%ERROR_CODE% 193 | 194 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost 195 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 196 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" 197 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" 198 | :skipRcPost 199 | 200 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 201 | if "%MAVEN_BATCH_PAUSE%"=="on" pause 202 | 203 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% 204 | 205 | cmd /C exit /B %ERROR_CODE% 206 | -------------------------------------------------------------------------------- /screenshots/logged-in-feed-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gtiwari333/spring-boot-blog-app/aa3d93f6e8e22b52bc7506a89de73bbbd5fdd26f/screenshots/logged-in-feed-view.png -------------------------------------------------------------------------------- /screenshots/logged-in-note-list-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gtiwari333/spring-boot-blog-app/aa3d93f6e8e22b52bc7506a89de73bbbd5fdd26f/screenshots/logged-in-note-list-view.png -------------------------------------------------------------------------------- /screenshots/public-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gtiwari333/spring-boot-blog-app/aa3d93f6e8e22b52bc7506a89de73bbbd5fdd26f/screenshots/public-view.png -------------------------------------------------------------------------------- /spot-bugs.filter-exclude.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/java/gt/app/Application.java: -------------------------------------------------------------------------------- 1 | package gt.app; 2 | 3 | import gt.app.config.AppProperties; 4 | import gt.app.config.Constants; 5 | import gt.app.config.security.AppUserDetails; 6 | import gt.app.modules.email.dto.EmailDto; 7 | import gt.app.modules.note.dto.NoteCreateDto; 8 | import gt.app.modules.note.dto.NoteEditDto; 9 | import gt.app.modules.note.dto.NoteReadDto; 10 | import gt.app.modules.user.AppPermissionEvaluatorService; 11 | import gt.app.modules.user.dto.PasswordUpdateDTO; 12 | import gt.app.modules.user.dto.UserDTO; 13 | import lombok.extern.slf4j.Slf4j; 14 | import org.springframework.aot.hint.MemberCategory; 15 | import org.springframework.aot.hint.RuntimeHints; 16 | import org.springframework.aot.hint.RuntimeHintsRegistrar; 17 | import org.springframework.boot.SpringApplication; 18 | import org.springframework.boot.autoconfigure.SpringBootApplication; 19 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 20 | import org.springframework.context.annotation.ImportRuntimeHints; 21 | import org.springframework.core.env.Environment; 22 | import org.springframework.data.domain.PageImpl; 23 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 24 | import org.springframework.transaction.annotation.EnableTransactionManagement; 25 | 26 | import java.net.InetAddress; 27 | import java.net.UnknownHostException; 28 | import java.util.Arrays; 29 | import java.util.Map; 30 | 31 | @SpringBootApplication 32 | @Slf4j 33 | @EnableConfigurationProperties(AppProperties.class) 34 | @EnableTransactionManagement(proxyTargetClass = true) 35 | @ImportRuntimeHints(MyRuntimeHints.class) //required for GraalVMNativeImage:: 36 | public class Application { 37 | 38 | public static void main(String[] args) throws UnknownHostException { 39 | 40 | var app = new SpringApplication(Application.class); 41 | app.setDefaultProperties(Map.of("spring.profiles.default", Constants.SPRING_PROFILE_DEVELOPMENT)); 42 | Environment env = app.run(args).getEnvironment(); 43 | 44 | log.info(""" 45 | Access URLs: 46 | ---------------------------------------------------------- 47 | \tLocal: \t\t\thttp://localhost:{} 48 | \tExternal: \t\thttp://{}:{} 49 | \tEnvironment: \t{}\s 50 | \t----------------------------------------------------------""", 51 | env.getProperty("server.port"), 52 | InetAddress.getLocalHost().getHostAddress(), 53 | env.getProperty("server.port"), 54 | Arrays.toString(env.getActiveProfiles()) 55 | ); 56 | } 57 | 58 | } 59 | 60 | //required for GraalVMNativeImage:: 61 | class MyRuntimeHints implements RuntimeHintsRegistrar { 62 | @Override 63 | public void registerHints(RuntimeHints hints, ClassLoader classLoader) { 64 | //record and dto classes -> get/set not found 65 | hints 66 | .reflection() 67 | .registerType(AppProperties.class, MemberCategory.values()) 68 | .registerType(AppProperties.FileStorage.class, MemberCategory.values()) 69 | .registerType(EmailDto.class, MemberCategory.values()) 70 | .registerType(EmailDto.FileBArray.class, MemberCategory.values()) 71 | .registerType(PasswordUpdateDTO.class, MemberCategory.values()) 72 | .registerType(UserDTO.class, MemberCategory.values()) 73 | .registerType(NoteCreateDto.class, MemberCategory.values()) 74 | .registerType(NoteEditDto.class, MemberCategory.values()) 75 | .registerType(NoteReadDto.class, MemberCategory.values()) 76 | .registerType(NoteReadDto.FileInfo.class, MemberCategory.values()) 77 | .registerType(AppUserDetails.class, MemberCategory.values()) 78 | .registerType(UsernamePasswordAuthenticationToken.class, MemberCategory.values()) 79 | .registerType(AppPermissionEvaluatorService.class, MemberCategory.values()) 80 | .registerType(PageImpl.class, MemberCategory.values()); //EL1004E: Method call: Method getTotalElements() cannot be found on type org.springframework.data.domain.PageImpl 81 | 82 | 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/gt/app/DataCreator.java: -------------------------------------------------------------------------------- 1 | package gt.app; 2 | 3 | import gt.app.config.AppProperties; 4 | import gt.app.config.Constants; 5 | import gt.app.domain.*; 6 | import gt.app.modules.note.NoteService; 7 | import gt.app.modules.user.AuthorityService; 8 | import gt.app.modules.user.UserService; 9 | import jakarta.persistence.EntityManager; 10 | import lombok.RequiredArgsConstructor; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.springframework.context.annotation.Profile; 13 | import org.springframework.context.event.ContextRefreshedEvent; 14 | import org.springframework.context.event.EventListener; 15 | import org.springframework.stereotype.Component; 16 | 17 | import java.io.File; 18 | import java.nio.file.Path; 19 | import java.util.stream.Stream; 20 | 21 | @Component 22 | @Profile({Constants.SPRING_PROFILE_DEVELOPMENT, Constants.SPRING_PROFILE_TEST, Constants.SPRING_PROFILE_DOCKER}) 23 | @RequiredArgsConstructor 24 | @Slf4j 25 | public class DataCreator { 26 | 27 | final AuthorityService authorityService; 28 | final UserService userService; 29 | final NoteService noteService; 30 | 31 | final EntityManager entityManager; 32 | 33 | final AppProperties appProperties; 34 | 35 | @EventListener 36 | public void ctxRefreshed(ContextRefreshedEvent evt) { 37 | initData(); 38 | } 39 | 40 | public void initData() { 41 | log.info("Context Refreshed !!, Initializing Data... "); 42 | 43 | File uploadFolder = Path.of(appProperties.fileStorage().uploadFolder()).toFile(); 44 | if (!uploadFolder.exists()) { 45 | if (uploadFolder.mkdirs() && Stream.of(ReceivedFile.FileGroup.values()).allMatch(f -> Path.of(uploadFolder.getAbsolutePath()).toFile().mkdir())) { 46 | log.info("Upload folder created successfully"); 47 | } else { 48 | log.info("Failure to create upload folder"); 49 | } 50 | } 51 | 52 | if (userService.existsByUniqueId("system")) { 53 | log.info("DB already initialized !!!"); 54 | return; 55 | } 56 | Authority adminAuthority = new Authority(); 57 | adminAuthority.setName(Constants.ROLE_ADMIN); 58 | authorityService.save(adminAuthority); 59 | 60 | Authority userAuthority = new Authority(); 61 | userAuthority.setName(Constants.ROLE_USER); 62 | authorityService.save(userAuthority); 63 | 64 | String pwd = "$2a$10$UtqWHf0BfCr41Nsy89gj4OCiL36EbTZ8g4o/IvFN2LArruHruiRXO"; // to make it faster //value is 'pass' 65 | 66 | AppUser adminUser = new AppUser("system", "System", "Tiwari", "system@email"); 67 | adminUser.setPassword(pwd); 68 | adminUser.setAuthorities(authorityService.findByNameIn(Constants.ROLE_ADMIN, Constants.ROLE_USER)); 69 | userService.save(adminUser); 70 | 71 | AppUser user1 = new AppUser("user1", "Ganesh", "Tiwari", "gt@email"); 72 | user1.setPassword(pwd); 73 | user1.setAuthorities(authorityService.findByNameIn(Constants.ROLE_USER)); 74 | userService.save(user1); 75 | 76 | 77 | AppUser user2 = new AppUser("user2", "Jyoti", "Kattel", "jk@email"); 78 | user2.setPassword(pwd); 79 | user2.setAuthorities(authorityService.findByNameIn(Constants.ROLE_USER)); 80 | userService.save(user2); 81 | 82 | createNote(adminUser, "Admin's First Note", "Content Admin 1"); 83 | createNote(adminUser, "Admin's Second Note", "Content Admin 2"); 84 | createNote(user1, "User1 Note", "Content User 1"); 85 | createNote(user2, "User2 Note", "Content User 2"); 86 | 87 | 88 | } 89 | 90 | void createNote(AppUser user, String title, String content) { 91 | var n = new Note(); 92 | n.setCreatedByUser(entityManager.getReference(LiteUser.class, user.getId())); 93 | n.setTitle(title); 94 | n.setContent(content); 95 | 96 | noteService.save(n); 97 | } 98 | 99 | 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/gt/app/config/AppProperties.java: -------------------------------------------------------------------------------- 1 | package gt.app.config; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | 5 | @ConfigurationProperties(prefix = "app-properties", ignoreUnknownFields = false) 6 | public record AppProperties(FileStorage fileStorage) { 7 | public record FileStorage(String uploadFolder) { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/gt/app/config/AuditorResolver.java: -------------------------------------------------------------------------------- 1 | package gt.app.config; 2 | 3 | import gt.app.config.security.SecurityUtils; 4 | import gt.app.domain.LiteUser; 5 | import jakarta.persistence.EntityManager; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.data.domain.AuditorAware; 8 | import org.springframework.stereotype.Component; 9 | 10 | import java.util.Optional; 11 | 12 | @Component 13 | @RequiredArgsConstructor 14 | public class AuditorResolver implements AuditorAware { 15 | 16 | private final EntityManager entityManager; 17 | 18 | @Override 19 | public Optional getCurrentAuditor() { 20 | 21 | Long userId = SecurityUtils.getCurrentUserId(); 22 | if (userId == null) { 23 | return Optional.empty(); 24 | } 25 | 26 | return Optional.ofNullable(entityManager.getReference(LiteUser.class, userId)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/gt/app/config/Constants.java: -------------------------------------------------------------------------------- 1 | package gt.app.config; 2 | 3 | public final class Constants { 4 | 5 | public static final String ROLE_ADMIN = "ROLE_ADMIN"; 6 | public static final String ROLE_USER = "ROLE_USER"; 7 | public static final String SPRING_PROFILE_DEVELOPMENT = "dev"; 8 | public static final String SPRING_PROFILE_TEST = "test"; 9 | public static final String SPRING_PROFILE_DOCKER = "docker"; 10 | 11 | private Constants() { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/gt/app/config/JpaConfig.java: -------------------------------------------------------------------------------- 1 | package gt.app.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing; 5 | 6 | @EnableJpaAuditing //now @CreatedBy, @LastModifiedBy works 7 | @Configuration 8 | public class JpaConfig { 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/gt/app/config/security/AppUserDetails.java: -------------------------------------------------------------------------------- 1 | package gt.app.config.security; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import gt.app.config.Constants; 5 | import gt.app.domain.Authority; 6 | import lombok.Getter; 7 | import org.springframework.security.core.GrantedAuthority; 8 | import org.springframework.security.core.userdetails.User; 9 | 10 | import java.util.Collection; 11 | import java.util.Objects; 12 | import java.util.stream.Collectors; 13 | 14 | @Getter 15 | public class AppUserDetails extends User { 16 | 17 | private final Long id; 18 | private final String firstName; 19 | 20 | private final String lastName; 21 | private final String email; 22 | 23 | public AppUserDetails(Long id, String userName, String email, String password, String firstName, String lastName, Collection authorities, 24 | boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked) { 25 | 26 | super(userName, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities); 27 | 28 | this.id = id; 29 | this.firstName = firstName; 30 | this.lastName = lastName; 31 | this.email = email; 32 | } 33 | 34 | @JsonIgnore 35 | public boolean isUser() { 36 | return getGrantedAuthorities().contains(Constants.ROLE_USER); 37 | } 38 | 39 | @JsonIgnore 40 | public boolean isSystemAdmin() { 41 | return getGrantedAuthorities().contains(Constants.ROLE_ADMIN); 42 | } 43 | 44 | public Collection getGrantedAuthorities() { 45 | Collection authorities = getAuthorities(); 46 | return authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet()); 47 | } 48 | 49 | @Override 50 | public boolean equals(Object o) { 51 | if (this == o) { 52 | return true; 53 | } 54 | if (o == null || getClass() != o.getClass()) { 55 | return false; 56 | } 57 | if (!super.equals(o)) { 58 | return false; 59 | } 60 | AppUserDetails that = (AppUserDetails) o; 61 | return id.equals(that.id); 62 | } 63 | 64 | @Override 65 | public int hashCode() { 66 | return Objects.hash(super.hashCode(), id); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/gt/app/config/security/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package gt.app.config.security; 2 | 3 | import gt.app.config.Constants; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; 8 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 9 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 10 | import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; 11 | import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; 12 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 13 | import org.springframework.security.crypto.password.PasswordEncoder; 14 | import org.springframework.security.web.SecurityFilterChain; 15 | import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; 16 | import org.springframework.security.web.util.matcher.AntPathRequestMatcher; 17 | import org.springframework.web.servlet.handler.HandlerMappingIntrospector; 18 | 19 | import java.util.stream.Stream; 20 | 21 | import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher; 22 | 23 | @EnableWebSecurity 24 | @Configuration 25 | @RequiredArgsConstructor 26 | @EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true) 27 | public class SecurityConfig { 28 | 29 | private static final String[] AUTH_WHITELIST = { 30 | "/swagger-resources/**", 31 | "/v3/api-docs/**", 32 | "/webjars/**", 33 | "/static/**", 34 | "/error/**", 35 | "/swagger-ui/**", 36 | "/swagger-ui.html/**", 37 | "/signup/**", 38 | "/" //landing page is allowed for all 39 | }; 40 | 41 | 42 | @Bean 43 | protected SecurityFilterChain filterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception { 44 | var mvcH2Console = new MvcRequestMatcher.Builder(introspector).servletPath("/h2-console"); 45 | http.headers(h -> h.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)) 46 | .authorizeHttpRequests(ah -> ah 47 | .requestMatchers(Stream.of(AUTH_WHITELIST).map(AntPathRequestMatcher::antMatcher).toList().toArray(new AntPathRequestMatcher[0])).permitAll() 48 | .requestMatchers(mvcH2Console.pattern("/**")).permitAll() 49 | .requestMatchers(antMatcher("/admin/**")).hasAuthority(Constants.ROLE_ADMIN) 50 | .requestMatchers(antMatcher("/user/**")).hasAuthority(Constants.ROLE_USER) 51 | .requestMatchers(antMatcher("/api/**")).authenticated()//individual api will be secured differently 52 | .anyRequest().authenticated()) 53 | .csrf(AbstractHttpConfigurer::disable) 54 | .formLogin(f -> f.loginProcessingUrl("/auth/login") 55 | .permitAll()) 56 | .logout(l -> l.logoutUrl("/auth/logout") 57 | .logoutSuccessUrl("/?logout") 58 | .permitAll()); 59 | 60 | return http.build(); 61 | } 62 | 63 | @Bean 64 | public PasswordEncoder passwordEncoder() { 65 | return new BCryptPasswordEncoder(); 66 | } 67 | 68 | } 69 | 70 | -------------------------------------------------------------------------------- /src/main/java/gt/app/config/security/SecurityUtils.java: -------------------------------------------------------------------------------- 1 | package gt.app.config.security; 2 | 3 | import org.springframework.security.core.Authentication; 4 | import org.springframework.security.core.context.SecurityContextHolder; 5 | import org.springframework.security.core.userdetails.User; 6 | 7 | /** 8 | * Utility class for Spring Security. 9 | */ 10 | public final class SecurityUtils { 11 | 12 | private SecurityUtils() { 13 | } 14 | 15 | /** 16 | * @return PK ( ID ) of current id 17 | */ 18 | public static Long getCurrentUserId() { 19 | 20 | User user = getCurrentUserDetails(); 21 | if (user instanceof AppUserDetails appUserDetails) { 22 | return appUserDetails.getId(); 23 | } 24 | return null; 25 | } 26 | 27 | public static User getCurrentUserDetails() { 28 | var authentication = SecurityContextHolder.getContext().getAuthentication(); 29 | return getCurrentUserDetails(authentication); 30 | } 31 | 32 | public static User getCurrentUserDetails(Authentication authentication) { 33 | User userDetails = null; 34 | if (authentication != null && authentication.getPrincipal() instanceof User) { 35 | userDetails = (User) authentication.getPrincipal(); 36 | } 37 | return userDetails; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/gt/app/domain/AppUser.java: -------------------------------------------------------------------------------- 1 | package gt.app.domain; 2 | 3 | import jakarta.persistence.*; 4 | import jakarta.validation.constraints.Size; 5 | import lombok.EqualsAndHashCode; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | import lombok.Setter; 9 | import org.hibernate.annotations.BatchSize; 10 | import org.hibernate.annotations.Fetch; 11 | import org.hibernate.annotations.FetchMode; 12 | import org.springframework.security.core.userdetails.UserDetails; 13 | 14 | import java.util.Collection; 15 | import java.util.HashSet; 16 | import java.util.Objects; 17 | import java.util.Set; 18 | 19 | @EqualsAndHashCode(callSuper = true) 20 | @Entity 21 | @Table(name = "APP_USER") 22 | @Getter 23 | @Setter 24 | @NoArgsConstructor 25 | public class AppUser extends BaseEntity implements UserDetails { 26 | @Basic(fetch = FetchType.LAZY) 27 | @Lob 28 | byte[] avatar; 29 | 30 | @Column(nullable = false) 31 | @Size(min = 2, max = 30) 32 | private String firstName; 33 | 34 | @Size(max = 30) 35 | private String lastName; 36 | 37 | @Column(length = 254, unique = true, nullable = false) 38 | private String email; 39 | 40 | @Column(nullable = false, unique = true) 41 | @Size(min = 5, max = 20) 42 | private String uniqueId; 43 | 44 | @Column(name = "password_hash", length = 60) 45 | private String password; 46 | 47 | @ManyToMany(fetch = FetchType.LAZY) 48 | @JoinTable( 49 | name = "user_authority", 50 | joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")}, 51 | inverseJoinColumns = {@JoinColumn(name = "authority_name", referencedColumnName = "name")}) 52 | @Fetch(FetchMode.SUBSELECT) 53 | @BatchSize(size = 5) 54 | private Set authorities = new HashSet<>(); 55 | 56 | @Column(nullable = false) 57 | private Boolean active = false; 58 | 59 | @Column(nullable = false) 60 | private Boolean accountNonExpired; 61 | 62 | @Column(nullable = false) 63 | private Boolean accountNonLocked; 64 | 65 | @Column(nullable = false) 66 | private Boolean credentialsNonExpired; 67 | 68 | private String activationKey; 69 | 70 | private String resetKey; 71 | 72 | @Override 73 | public Collection getAuthorities() { 74 | return authorities; 75 | } 76 | 77 | @Override 78 | public String getPassword() { 79 | return password; 80 | } 81 | 82 | @Override 83 | public String getUsername() { 84 | return uniqueId; 85 | } 86 | 87 | @Override 88 | public boolean isAccountNonExpired() { 89 | return accountNonExpired; 90 | } 91 | 92 | @Override 93 | public boolean isAccountNonLocked() { 94 | return accountNonLocked; 95 | } 96 | 97 | @Override 98 | public boolean isCredentialsNonExpired() { 99 | return credentialsNonExpired; 100 | } 101 | 102 | @Override 103 | public boolean isEnabled() { 104 | return active; 105 | } 106 | 107 | public AppUser(String uniqueId, String firstName, String lastName, String email) { 108 | this.uniqueId = uniqueId; 109 | this.firstName = firstName; 110 | this.lastName = lastName; 111 | this.email = email; 112 | this.accountNonExpired = Boolean.TRUE; 113 | this.accountNonLocked = Boolean.TRUE; 114 | this.credentialsNonExpired = Boolean.TRUE; 115 | this.active = Boolean.TRUE; 116 | } 117 | 118 | @Override 119 | public String toString() { 120 | return "User{" + 121 | "firstName='" + firstName + '\'' + 122 | ", lastName='" + lastName + '\'' + 123 | ", email='" + email + '\'' + 124 | ", authorities=" + authorities + 125 | ", active=" + active + 126 | ", accountNonExpired=" + accountNonExpired + 127 | ", accountNonLocked=" + accountNonLocked + 128 | ", credentialsNonExpired=" + credentialsNonExpired + 129 | '}'; 130 | } 131 | 132 | @Override 133 | public boolean equals(Object o) { 134 | if (this == o) { 135 | return true; 136 | } 137 | if (o == null || getClass() != o.getClass()) { 138 | return false; 139 | } 140 | if (!super.equals(o)) { 141 | return false; 142 | } 143 | AppUser appUser = (AppUser) o; 144 | return Objects.equals(uniqueId, appUser.uniqueId); 145 | } 146 | 147 | @Override 148 | public int hashCode() { 149 | return Objects.hash(super.hashCode(), uniqueId); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/main/java/gt/app/domain/Authority.java: -------------------------------------------------------------------------------- 1 | package gt.app.domain; 2 | 3 | import lombok.Setter; 4 | import org.springframework.security.core.GrantedAuthority; 5 | 6 | import jakarta.persistence.Column; 7 | import jakarta.persistence.Entity; 8 | import jakarta.persistence.Id; 9 | 10 | @Entity 11 | @Setter 12 | public class Authority implements GrantedAuthority { 13 | 14 | @Id 15 | @Column(length = 16) 16 | private String name; 17 | 18 | @Override 19 | public String getAuthority() { 20 | return name; 21 | } 22 | 23 | public Authority() { 24 | } 25 | 26 | public Authority(String name) { 27 | this.name = name; 28 | } 29 | 30 | @Override 31 | public boolean equals(Object o) { 32 | if (this == o) { 33 | return true; 34 | } 35 | if (o == null || getClass() != o.getClass()) { 36 | return false; 37 | } 38 | 39 | Authority authority1 = (Authority) o; 40 | 41 | return name.equals(authority1.name); 42 | } 43 | 44 | @Override 45 | public int hashCode() { 46 | return name.hashCode(); 47 | } 48 | 49 | @Override 50 | public String toString() { 51 | return """ 52 | Authority{\ 53 | authority='\ 54 | """ + name + '\'' + 55 | '}'; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/gt/app/domain/BaseAuditingEntity.java: -------------------------------------------------------------------------------- 1 | package gt.app.domain; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.springframework.data.annotation.CreatedBy; 7 | import org.springframework.data.annotation.CreatedDate; 8 | import org.springframework.data.annotation.LastModifiedBy; 9 | import org.springframework.data.annotation.LastModifiedDate; 10 | import org.springframework.data.jpa.domain.support.AuditingEntityListener; 11 | 12 | import jakarta.persistence.*; 13 | 14 | import java.time.Instant; 15 | 16 | @MappedSuperclass 17 | @EntityListeners(AuditingEntityListener.class) 18 | @Getter 19 | @Setter 20 | abstract class BaseAuditingEntity extends BaseEntity { 21 | 22 | private static final long serialVersionUID = 4681401402666658611L; 23 | 24 | @CreatedBy 25 | @ManyToOne(fetch = FetchType.LAZY) 26 | @JoinColumn(name = "created_by_user_id", updatable = false, nullable = false) 27 | @JsonIgnore//ignore completely to avoid StackOverflow exception by User.createdByUser logic, use DTO 28 | private LiteUser createdByUser; 29 | 30 | @CreatedDate 31 | @Column(name = "created_date", updatable = false, nullable = false) 32 | private Instant createdDate; 33 | 34 | @LastModifiedBy 35 | @ManyToOne(fetch = FetchType.LAZY) 36 | @JoinColumn(name = "last_modified_by_user_id") 37 | @JsonIgnore//ignore completely to avoid StackOverflow exception by User.lastModifiedByUser logic, use DTO 38 | private LiteUser lastModifiedByUser; 39 | 40 | @LastModifiedDate 41 | @Column(name = "last_modified_date") 42 | private Instant lastModifiedDate; 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/gt/app/domain/BaseEntity.java: -------------------------------------------------------------------------------- 1 | package gt.app.domain; 2 | 3 | import lombok.Data; 4 | 5 | import jakarta.persistence.GeneratedValue; 6 | import jakarta.persistence.Id; 7 | import jakarta.persistence.MappedSuperclass; 8 | 9 | @Data 10 | @MappedSuperclass 11 | public abstract class BaseEntity { 12 | 13 | @Id 14 | @GeneratedValue 15 | protected Long id; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/gt/app/domain/LiteUser.java: -------------------------------------------------------------------------------- 1 | package gt.app.domain; 2 | 3 | import lombok.Data; 4 | import lombok.EqualsAndHashCode; 5 | 6 | import jakarta.persistence.*; 7 | import jakarta.validation.constraints.Size; 8 | 9 | @EqualsAndHashCode(callSuper = true) 10 | @Entity 11 | @Table(name="APP_USER") 12 | @Data 13 | public class LiteUser extends BaseEntity { 14 | 15 | @Column(nullable = false) 16 | @Size(min = 2, max = 30) 17 | private String firstName; 18 | 19 | @Size(max = 30) 20 | private String lastName; 21 | 22 | @Column(length = 254, unique = true, nullable = false) 23 | private String email; 24 | 25 | @Column(nullable = false, unique = true) 26 | @Size(min = 5, max = 20) 27 | private String uniqueId; 28 | 29 | @Column(name = "password_hash", length = 60) 30 | private String password; 31 | 32 | @Column(nullable = false) 33 | private Boolean active = false; 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/gt/app/domain/Note.java: -------------------------------------------------------------------------------- 1 | package gt.app.domain; 2 | 3 | import lombok.Data; 4 | import lombok.EqualsAndHashCode; 5 | import org.hibernate.annotations.BatchSize; 6 | import org.hibernate.annotations.Fetch; 7 | import org.hibernate.annotations.FetchMode; 8 | 9 | import jakarta.persistence.*; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | @EqualsAndHashCode(callSuper = true) 14 | @Entity 15 | @Table(name = "note") 16 | @Data 17 | public class Note extends BaseAuditingEntity { 18 | 19 | private String title; 20 | 21 | private String content; 22 | 23 | @OneToMany(cascade = CascadeType.PERSIST, fetch = FetchType.LAZY) 24 | @Fetch(FetchMode.SUBSELECT) 25 | @BatchSize(size = 5) 26 | private List attachedFiles = new ArrayList<>(); 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/gt/app/domain/ReceivedFile.java: -------------------------------------------------------------------------------- 1 | package gt.app.domain; 2 | 3 | import lombok.Data; 4 | import lombok.NoArgsConstructor; 5 | 6 | import jakarta.persistence.Entity; 7 | import jakarta.persistence.EnumType; 8 | import jakarta.persistence.Enumerated; 9 | import jakarta.persistence.Id; 10 | import java.time.Instant; 11 | import java.util.UUID; 12 | 13 | @Entity 14 | @Data 15 | @NoArgsConstructor 16 | public class ReceivedFile { 17 | 18 | @Id 19 | UUID id; 20 | 21 | Instant receivedDate; 22 | String originalFileName; 23 | 24 | String storedName; 25 | 26 | @Enumerated(EnumType.STRING) 27 | FileGroup fileGroup; 28 | 29 | public ReceivedFile(FileGroup group, String originalFileName, String storedName) { 30 | this.fileGroup = group; 31 | this.originalFileName = originalFileName; 32 | this.storedName = storedName; 33 | this.id = UUID.randomUUID(); 34 | } 35 | 36 | public enum FileGroup { 37 | NOTE_ATTACHMENT, 38 | //add other 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/gt/app/exception/InvalidDataException.java: -------------------------------------------------------------------------------- 1 | package gt.app.exception; 2 | 3 | import java.io.Serial; 4 | 5 | public class InvalidDataException extends RuntimeException { 6 | 7 | @Serial 8 | private static final long serialVersionUID = 1L; 9 | 10 | public InvalidDataException(String message) { 11 | super(message); 12 | } 13 | 14 | public InvalidDataException(String message, Throwable e) { 15 | super(message, e); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/gt/app/exception/OperationNotAllowedException.java: -------------------------------------------------------------------------------- 1 | package gt.app.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(value = HttpStatus.BAD_REQUEST) 7 | public class OperationNotAllowedException extends RuntimeException { 8 | 9 | public OperationNotAllowedException(String what) { 10 | super(what); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/gt/app/exception/RecordNotFoundException.java: -------------------------------------------------------------------------------- 1 | package gt.app.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(value = HttpStatus.NOT_FOUND) 7 | public class RecordNotFoundException extends RuntimeException { 8 | 9 | public RecordNotFoundException(String description) { 10 | super(description); 11 | } 12 | 13 | public RecordNotFoundException(String requestedObjectName, String requestedByField, Object requestedByParam) { 14 | super("%s not found with %s = '%s'".formatted(requestedObjectName, requestedByField, requestedByParam)); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/gt/app/modules/email/EmailService.java: -------------------------------------------------------------------------------- 1 | package gt.app.modules.email; 2 | 3 | import gt.app.modules.email.dto.EmailDto; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.core.io.ByteArrayResource; 7 | import org.springframework.mail.MailException; 8 | import org.springframework.mail.javamail.JavaMailSender; 9 | import org.springframework.mail.javamail.MimeMessageHelper; 10 | import org.springframework.stereotype.Service; 11 | 12 | import jakarta.mail.MessagingException; 13 | import java.io.IOException; 14 | import java.nio.charset.StandardCharsets; 15 | 16 | import static gt.app.modules.email.EmailUtil.toInetArray; 17 | 18 | @Service 19 | @Slf4j 20 | @RequiredArgsConstructor 21 | public class EmailService { 22 | 23 | private final JavaMailSender javaMailSender; 24 | 25 | public void sendEmail(EmailDto email) { 26 | 27 | try { 28 | var mimeMessage = javaMailSender.createMimeMessage(); 29 | var message = new MimeMessageHelper(mimeMessage, true, StandardCharsets.UTF_8.name()); 30 | 31 | message.setTo(toInetArray(email.to())); 32 | message.setCc(toInetArray(email.cc())); 33 | message.setBcc(toInetArray(email.bcc())); 34 | message.setFrom(email.from(), email.from()); 35 | 36 | message.setSubject(email.subject()); 37 | message.setText(email.content(), email.isHtml()); 38 | 39 | if (email.files() != null) { 40 | for (var file : email.files()) { 41 | message.addAttachment(file.filename(), new ByteArrayResource(file.data())); 42 | } 43 | } 44 | 45 | javaMailSender.send(mimeMessage); 46 | 47 | log.debug("Email Sent subject: {}", email.subject()); 48 | } catch (MailException | IOException | MessagingException e) { 49 | log.error("Failed to send email", e); 50 | } 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/gt/app/modules/email/EmailUtil.java: -------------------------------------------------------------------------------- 1 | package gt.app.modules.email; 2 | 3 | import gt.app.exception.InvalidDataException; 4 | 5 | import jakarta.mail.internet.AddressException; 6 | import jakarta.mail.internet.InternetAddress; 7 | import java.util.Collection; 8 | import java.util.function.Function; 9 | 10 | public class EmailUtil { 11 | 12 | static Function toInternetAddr() { 13 | return it -> { 14 | try { 15 | return new InternetAddress(it); 16 | } catch (AddressException e) { 17 | throw new InvalidDataException("Invalid email address " + it, e); 18 | } 19 | }; 20 | } 21 | 22 | static InternetAddress[] toInetArray(Collection tos) { 23 | if (tos == null) { 24 | return new InternetAddress[0]; 25 | } 26 | return tos.stream().map(EmailUtil.toInternetAddr()).toArray(InternetAddress[]::new); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/gt/app/modules/email/dto/EmailDto.java: -------------------------------------------------------------------------------- 1 | package gt.app.modules.email.dto; 2 | 3 | import java.util.Collection; 4 | import java.util.List; 5 | 6 | public record EmailDto(String from, Collection to, Collection cc, Collection bcc, 7 | String subject, String content, boolean isHtml, FileBArray[] files) { 8 | 9 | public static EmailDto of(String from, Collection to, String subject, String content) { 10 | return new EmailDto(from, to, List.of(), List.of(), subject, content, false, new FileBArray[]{}); 11 | } 12 | 13 | public record FileBArray(byte[] data, String filename) { 14 | } 15 | 16 | @Override 17 | public String toString() { 18 | return "EmailDto{" + 19 | "from='" + from + '\'' + 20 | ", to=" + to + 21 | ", cc=" + cc + 22 | ", bcc=" + bcc + 23 | ", subject='" + subject + '\'' + 24 | '}'; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/gt/app/modules/file/FileDownloadUtil.java: -------------------------------------------------------------------------------- 1 | package gt.app.modules.file; 2 | 3 | import jakarta.servlet.http.HttpServletResponse; 4 | import org.springframework.util.MimeTypeUtils; 5 | 6 | import java.io.BufferedInputStream; 7 | import java.io.IOException; 8 | import java.net.URL; 9 | 10 | public final class FileDownloadUtil { 11 | 12 | private FileDownloadUtil() { 13 | } 14 | 15 | public static void downloadFile(HttpServletResponse response, URL file, String originalFileName) throws IOException { 16 | handle(response, file, originalFileName, null); 17 | } 18 | 19 | public static void downloadFile(HttpServletResponse response, URL file, String originalFileName, String mimeType) throws IOException { 20 | handle(response, file, originalFileName, mimeType); 21 | } 22 | 23 | 24 | private static void handle(HttpServletResponse response, URL file, String originalFileName, String mimeType) throws IOException { 25 | try (var in = new BufferedInputStream(file.openStream())) { 26 | 27 | // get MIME type of the file 28 | 29 | if (mimeType == null) { 30 | // set to binary type if MIME mapping not found 31 | mimeType = MimeTypeUtils.APPLICATION_OCTET_STREAM.getType(); 32 | } 33 | 34 | // set content attributes for the response 35 | response.setContentType(mimeType); 36 | //response.setContentLength((int) file.length()); 37 | 38 | // This will download the file to the user's computer 39 | response.setHeader("Content-Disposition", "attachment; filename=" + originalFileName); 40 | 41 | in.transferTo(response.getOutputStream()); 42 | 43 | response.getOutputStream().flush(); 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/gt/app/modules/file/FileService.java: -------------------------------------------------------------------------------- 1 | package gt.app.modules.file; 2 | 3 | import gt.app.config.AppProperties; 4 | import gt.app.domain.ReceivedFile; 5 | import org.springframework.core.io.Resource; 6 | import org.springframework.core.io.UrlResource; 7 | import org.springframework.stereotype.Service; 8 | import org.springframework.web.multipart.MultipartFile; 9 | 10 | import jakarta.validation.constraints.NotNull; 11 | import java.io.IOException; 12 | import java.nio.file.Path; 13 | import java.util.UUID; 14 | 15 | @Service 16 | public class FileService { 17 | 18 | private final Path rootLocation; 19 | 20 | public FileService(AppProperties appProperties) { 21 | this.rootLocation = Path.of(appProperties.fileStorage().uploadFolder()); 22 | } 23 | 24 | public String store(ReceivedFile.FileGroup fileGroup, @NotNull MultipartFile file) { 25 | 26 | try { 27 | 28 | String fileIdentifier = getCleanedFileName(file.getOriginalFilename()); 29 | 30 | Path targetPath = getStoredFilePath(fileGroup, fileIdentifier); 31 | 32 | file.transferTo(targetPath); 33 | 34 | return fileIdentifier; 35 | 36 | } catch (IOException e) { 37 | throw new StorageException("Failed to store file " + file, e); 38 | } 39 | } 40 | 41 | public Resource loadAsResource(ReceivedFile.FileGroup fileGroup, String fileIdentifier) { 42 | try { 43 | Path targetPath = getStoredFilePath(fileGroup, fileIdentifier); 44 | 45 | Resource resource = new UrlResource(targetPath.toUri()); 46 | if (resource.exists() || resource.isReadable()) { 47 | return resource; 48 | } else { 49 | throw new IOException("Could not read file: " + targetPath); 50 | 51 | } 52 | } catch (IOException e) { 53 | throw new RetrievalException("Could not read file: " + fileIdentifier + " , group " + fileGroup, e); 54 | } 55 | } 56 | 57 | private String getCleanedFileName(String originalName) throws IOException { 58 | if (originalName == null || originalName.isEmpty()) { 59 | throw new IOException("Failed to store empty file " + originalName); 60 | } 61 | 62 | if (originalName.contains("..")) { 63 | // This is a security check 64 | throw new IOException("Cannot store file with relative path outside current directory " + originalName); 65 | } 66 | 67 | return UUID.randomUUID().toString(); 68 | } 69 | 70 | private String getSubFolder(ReceivedFile.FileGroup fileGroup) throws IOException { 71 | if (fileGroup == ReceivedFile.FileGroup.NOTE_ATTACHMENT) { 72 | return "attachments"; 73 | } 74 | 75 | throw new IOException("File group subfolder " + fileGroup + " is not implemented"); 76 | } 77 | 78 | private Path getStoredFilePath(ReceivedFile.FileGroup fileGroup, String fileIdentifier) throws IOException { 79 | return rootLocation.resolve(getSubFolder(fileGroup)).resolve(fileIdentifier); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/gt/app/modules/file/ReceivedFileRepository.java: -------------------------------------------------------------------------------- 1 | package gt.app.modules.file; 2 | 3 | import gt.app.domain.ReceivedFile; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.util.UUID; 7 | 8 | interface ReceivedFileRepository extends JpaRepository { 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/gt/app/modules/file/ReceivedFileService.java: -------------------------------------------------------------------------------- 1 | package gt.app.modules.file; 2 | 3 | import gt.app.domain.ReceivedFile; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.stereotype.Service; 6 | 7 | import java.util.Optional; 8 | import java.util.UUID; 9 | 10 | @Service 11 | @RequiredArgsConstructor 12 | public class ReceivedFileService { 13 | 14 | final ReceivedFileRepository receivedFileRepository; 15 | 16 | public Optional findById(UUID id) { 17 | return receivedFileRepository.findById(id); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/gt/app/modules/file/RetrievalException.java: -------------------------------------------------------------------------------- 1 | package gt.app.modules.file; 2 | 3 | public class RetrievalException extends RuntimeException { 4 | public RetrievalException(String message, Throwable cause) { 5 | super(message, cause); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/gt/app/modules/file/StorageException.java: -------------------------------------------------------------------------------- 1 | package gt.app.modules.file; 2 | 3 | public class StorageException extends RuntimeException { 4 | public StorageException(String message, Throwable cause) { 5 | super(message, cause); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/gt/app/modules/note/NoteRepository.java: -------------------------------------------------------------------------------- 1 | package gt.app.modules.note; 2 | 3 | import gt.app.domain.Note; 4 | import org.springframework.data.domain.Page; 5 | import org.springframework.data.domain.Pageable; 6 | import org.springframework.data.jpa.repository.EntityGraph; 7 | import org.springframework.data.jpa.repository.JpaRepository; 8 | import org.springframework.data.jpa.repository.Query; 9 | import org.springframework.data.repository.query.Param; 10 | 11 | import java.util.Optional; 12 | 13 | interface NoteRepository extends JpaRepository { 14 | 15 | @EntityGraph(attributePaths = {"createdByUser", "attachedFiles"}) 16 | Optional findById(Long id); 17 | 18 | @EntityGraph(attributePaths = {"createdByUser", "attachedFiles"}) 19 | Page findAll(Pageable pageable); 20 | 21 | @EntityGraph(attributePaths = {"createdByUser", "attachedFiles"}) 22 | Page findByCreatedByUserIdOrderByCreatedDateDesc(Pageable pageable, Long userId); 23 | 24 | @Query("select n.createdByUser.id from Note n where n.id=:id ") 25 | Long findCreatedByUserIdById(@Param("id") Long id); 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/gt/app/modules/note/NoteService.java: -------------------------------------------------------------------------------- 1 | package gt.app.modules.note; 2 | 3 | import gt.app.domain.Note; 4 | import gt.app.domain.ReceivedFile; 5 | import gt.app.modules.file.FileService; 6 | import gt.app.modules.note.dto.NoteCreateDto; 7 | import gt.app.modules.note.dto.NoteEditDto; 8 | import gt.app.modules.note.dto.NoteMapper; 9 | import gt.app.modules.note.dto.NoteReadDto; 10 | import lombok.RequiredArgsConstructor; 11 | import org.springframework.data.domain.Page; 12 | import org.springframework.data.domain.Pageable; 13 | import org.springframework.stereotype.Service; 14 | import org.springframework.web.multipart.MultipartFile; 15 | 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | import java.util.Optional; 19 | 20 | @Service 21 | @RequiredArgsConstructor 22 | public class NoteService { 23 | 24 | private static final ReceivedFile.FileGroup FILE_GROUP = ReceivedFile.FileGroup.NOTE_ATTACHMENT; 25 | private final NoteRepository noteRepository; 26 | private final FileService fileService; 27 | 28 | private final NoteMapper noteMapper; 29 | 30 | public Note createNote(NoteCreateDto dto) { 31 | 32 | List files = new ArrayList<>(); 33 | for (MultipartFile mpf : dto.files()) { 34 | 35 | if (mpf.isEmpty()) { 36 | continue; 37 | } 38 | 39 | String fileId = fileService.store(FILE_GROUP, mpf); 40 | files.add(new ReceivedFile(FILE_GROUP, mpf.getOriginalFilename(), fileId)); 41 | } 42 | 43 | Note note = noteMapper.createToEntity(dto); 44 | note.getAttachedFiles().addAll(files); 45 | 46 | return save(note); 47 | } 48 | 49 | public Note update(NoteEditDto dto) { 50 | 51 | Optional noteOpt = noteRepository.findById(dto.id()); 52 | return noteOpt.map(note -> { 53 | noteMapper.createToEntity(dto, note); 54 | return save(note); 55 | }).orElseThrow(); 56 | } 57 | 58 | public NoteReadDto read(Long id) { 59 | return noteRepository.findById(id) 60 | .map(noteMapper::mapForRead).orElseThrow(); 61 | } 62 | 63 | public Note save(Note note) { 64 | return noteRepository.save(note); 65 | } 66 | 67 | public Page readAll(Pageable pageable) { 68 | return noteRepository.findAll(pageable) 69 | .map(noteMapper::mapForRead); 70 | } 71 | 72 | public Page readAllByUser(Pageable pageable, Long userId) { 73 | return noteRepository.findByCreatedByUserIdOrderByCreatedDateDesc(pageable, userId) 74 | .map(noteMapper::mapForRead); 75 | } 76 | 77 | public void delete(Long id) { 78 | noteRepository.deleteById(id); 79 | } 80 | 81 | public Long findCreatedByUserIdById(Long id) { 82 | return noteRepository.findCreatedByUserIdById(id); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/gt/app/modules/note/dto/NoteCreateDto.java: -------------------------------------------------------------------------------- 1 | package gt.app.modules.note.dto; 2 | 3 | import org.springframework.web.multipart.MultipartFile; 4 | 5 | import jakarta.validation.constraints.NotNull; 6 | 7 | public record NoteCreateDto(@NotNull MultipartFile[] files, String title, String content) { 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/gt/app/modules/note/dto/NoteEditDto.java: -------------------------------------------------------------------------------- 1 | package gt.app.modules.note.dto; 2 | 3 | public record NoteEditDto(Long id, String title, String content) { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/java/gt/app/modules/note/dto/NoteMapper.java: -------------------------------------------------------------------------------- 1 | package gt.app.modules.note.dto; 2 | 3 | import gt.app.domain.Note; 4 | import gt.app.domain.ReceivedFile; 5 | import org.mapstruct.Mapper; 6 | import org.mapstruct.Mapping; 7 | import org.mapstruct.MappingTarget; 8 | 9 | @Mapper(componentModel = "spring") 10 | public interface NoteMapper { 11 | 12 | @Mapping(source = "createdByUser.id", target = "userId") 13 | @Mapping(source = "createdByUser.uniqueId", target = "username") 14 | @Mapping(source = "attachedFiles", target = "files") 15 | NoteReadDto mapForRead(Note note); 16 | 17 | @Mapping(target = "id", ignore = true) 18 | @Mapping(target = "attachedFiles", ignore = true) 19 | void createToEntity(NoteEditDto dto, @MappingTarget Note note); 20 | 21 | Note createToEntity(NoteCreateDto dto); 22 | 23 | @Mapping(source = "originalFileName", target = "name") 24 | NoteReadDto.FileInfo map(ReceivedFile receivedFile); 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/gt/app/modules/note/dto/NoteReadDto.java: -------------------------------------------------------------------------------- 1 | package gt.app.modules.note.dto; 2 | 3 | import java.time.Instant; 4 | import java.util.List; 5 | import java.util.UUID; 6 | 7 | public record NoteReadDto(Long id, String title, String content, Long userId, String username, Instant createdDate, 8 | List files) { 9 | 10 | ////required for GraalVMNativeImage:: 11 | //SpelEvaluationException: EL1004E: Method call: Method size() cannot be found on type java.util.ArrayList 12 | //Caused by: org.thymeleaf.exceptions.TemplateProcessingException: Exception evaluating SpringEL expression: "note.files.size()>0" (template: "note/_notes" - line 43, col 18) 13 | public int getFileSize() { 14 | if (files == null) { 15 | return 0; 16 | } 17 | return files.size(); 18 | } 19 | 20 | public record FileInfo(UUID id, String name) { 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/gt/app/modules/user/AppPermissionEvaluatorService.java: -------------------------------------------------------------------------------- 1 | package gt.app.modules.user; 2 | 3 | import gt.app.config.security.SecurityUtils; 4 | import gt.app.config.security.AppUserDetails; 5 | import gt.app.domain.BaseEntity; 6 | import gt.app.exception.OperationNotAllowedException; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.security.access.PermissionEvaluator; 10 | import org.springframework.security.core.Authentication; 11 | import org.springframework.security.core.userdetails.User; 12 | import org.springframework.stereotype.Service; 13 | 14 | import java.io.Serializable; 15 | 16 | @Slf4j 17 | @RequiredArgsConstructor 18 | @Service("permEvaluator") 19 | public class AppPermissionEvaluatorService implements PermissionEvaluator { 20 | 21 | private final UserAuthorityService userAuthorityService; 22 | 23 | @Override 24 | public boolean hasPermission(Authentication auth, Object targetDomainObject, Object permission) { 25 | 26 | if ((auth == null) || (targetDomainObject == null)) { 27 | log.warn("Either auth or targetDomainObject null "); 28 | return false; 29 | } 30 | 31 | String targetType = targetDomainObject.getClass().getSimpleName(); 32 | Long targetId = ((BaseEntity) targetDomainObject).getId(); 33 | 34 | return hasAccess(targetId, targetType); 35 | } 36 | 37 | @Override 38 | public boolean hasPermission(Authentication auth, Serializable targetId, String targetType, Object permission) { 39 | if (targetId instanceof Long longTargetId) { 40 | return hasAccess(longTargetId, targetType); 41 | } else { 42 | throw new OperationNotAllowedException("Invalid id type " + targetId.getClass() + ". Expected Long"); 43 | } 44 | } 45 | 46 | public boolean hasAccess(Long id, String targetEntity) { 47 | User curUser = SecurityUtils.getCurrentUserDetails(); 48 | 49 | if (!(curUser instanceof AppUserDetails appUser)) { 50 | throw new OperationNotAllowedException("Current SecurityContext doesn't have AppUserDetails "); 51 | } 52 | 53 | return userAuthorityService.hasAccess(appUser, id, targetEntity); 54 | } 55 | 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/gt/app/modules/user/AppUserDetailsService.java: -------------------------------------------------------------------------------- 1 | package gt.app.modules.user; 2 | 3 | import gt.app.config.security.AppUserDetails; 4 | import gt.app.domain.AppUser; 5 | import lombok.AllArgsConstructor; 6 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 7 | import org.springframework.stereotype.Service; 8 | 9 | import java.util.Optional; 10 | 11 | @Service 12 | @AllArgsConstructor 13 | public class AppUserDetailsService implements org.springframework.security.core.userdetails.UserDetailsService { 14 | 15 | private final UserRepository userRepository; 16 | 17 | @Override 18 | public AppUserDetails loadUserByUsername(String email) { 19 | Optional userFromDatabase = userRepository.findOneWithAuthoritiesByUniqueId(email); 20 | 21 | return userFromDatabase 22 | .map(this::getCustomUserDetails) 23 | .orElseThrow(() -> new UsernameNotFoundException(" User with login:" + email + " was not found in the " + " database ")); 24 | } 25 | 26 | public AppUserDetails getCustomUserDetails(AppUser user) { 27 | 28 | return new AppUserDetails(user.getId(), user.getUsername(), user.getEmail(), user.getPassword(), user.getFirstName(), user.getLastName(), user.getAuthorities(), 29 | user.isEnabled(), user.isAccountNonExpired(), user.isCredentialsNonExpired(), user.isAccountNonLocked()); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/gt/app/modules/user/AuthorityRepository.java: -------------------------------------------------------------------------------- 1 | package gt.app.modules.user; 2 | 3 | import gt.app.domain.Authority; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.util.Collection; 7 | import java.util.Set; 8 | 9 | interface AuthorityRepository extends JpaRepository { 10 | Set findByNameIn(Collection name); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/gt/app/modules/user/AuthorityService.java: -------------------------------------------------------------------------------- 1 | package gt.app.modules.user; 2 | 3 | import gt.app.domain.Authority; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.stereotype.Service; 6 | 7 | import java.util.List; 8 | import java.util.Set; 9 | 10 | @Service 11 | @RequiredArgsConstructor 12 | public class AuthorityService { 13 | 14 | final AuthorityRepository authorityRepository; 15 | 16 | public void save(Authority auth) { 17 | authorityRepository.save(auth); 18 | } 19 | 20 | public Set findByNameIn(String... roles) { 21 | return authorityRepository.findByNameIn(List.of(roles)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/gt/app/modules/user/LiteUserRepository.java: -------------------------------------------------------------------------------- 1 | package gt.app.modules.user; 2 | 3 | import gt.app.domain.LiteUser; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.util.Optional; 7 | 8 | interface LiteUserRepository extends JpaRepository { 9 | Optional findOneByUniqueId(String uniqueId); 10 | 11 | Optional findByIdAndActiveIsTrue(Long id); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/gt/app/modules/user/PasswordUpdateValidator.java: -------------------------------------------------------------------------------- 1 | package gt.app.modules.user; 2 | 3 | import gt.app.modules.user.dto.PasswordUpdateDTO; 4 | import org.apache.commons.lang3.StringUtils; 5 | import org.springframework.security.core.userdetails.UserDetails; 6 | import org.springframework.stereotype.Component; 7 | import org.springframework.validation.Errors; 8 | import org.springframework.validation.Validator; 9 | 10 | @Component 11 | public class PasswordUpdateValidator implements Validator { 12 | 13 | @Override 14 | public boolean supports(Class clazz) { 15 | return PasswordUpdateDTO.class.equals(clazz); 16 | } 17 | 18 | @Override 19 | public void validate(Object target, Errors errors) { 20 | } 21 | 22 | public void validate(Object target, Errors errors, UserDetails principal) { 23 | 24 | PasswordUpdateDTO toCreate = (PasswordUpdateDTO) target; 25 | 26 | if (StringUtils.containsIgnoreCase(toCreate.pwdPlainText(), principal.getUsername()) || StringUtils.containsIgnoreCase(principal.getUsername(), toCreate.pwdPlainText())) { 27 | errors.rejectValue("pwdPlaintext", "user.weakpwd", "Weak password, choose another"); 28 | } 29 | 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/gt/app/modules/user/UserAuthorityService.java: -------------------------------------------------------------------------------- 1 | package gt.app.modules.user; 2 | 3 | import gt.app.config.security.AppUserDetails; 4 | import gt.app.domain.AppUser; 5 | import gt.app.domain.Note; 6 | import gt.app.modules.note.NoteService; 7 | import lombok.RequiredArgsConstructor; 8 | import org.springframework.stereotype.Service; 9 | 10 | @Service("appSecurity") 11 | @RequiredArgsConstructor 12 | public class UserAuthorityService { 13 | 14 | private final NoteService noteService; 15 | 16 | public boolean hasAccess(AppUserDetails curUser, Long id, String entity) { 17 | 18 | if (curUser.isSystemAdmin()) { 19 | return true; 20 | } 21 | 22 | if (AppUser.class.getSimpleName().equalsIgnoreCase(entity)) { 23 | return id.equals(curUser.getId()); 24 | } 25 | 26 | 27 | if (Note.class.getSimpleName().equalsIgnoreCase(entity)) { 28 | 29 | Long createdById = noteService.findCreatedByUserIdById(id); 30 | 31 | return createdById.equals(curUser.getId()); 32 | } 33 | 34 | 35 | /* 36 | add more rules 37 | */ 38 | 39 | return false; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/gt/app/modules/user/UserRepository.java: -------------------------------------------------------------------------------- 1 | package gt.app.modules.user; 2 | 3 | import gt.app.domain.AppUser; 4 | import org.springframework.data.jpa.repository.EntityGraph; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | import java.util.Optional; 8 | 9 | interface UserRepository extends JpaRepository { 10 | 11 | @EntityGraph(attributePaths = {"authorities"}) 12 | Optional findOneWithAuthoritiesByUniqueId(String uniqueId); 13 | 14 | boolean existsByUniqueId(String uniqueId); 15 | 16 | Optional findByIdAndActiveIsTrue(Long id); 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/gt/app/modules/user/UserService.java: -------------------------------------------------------------------------------- 1 | package gt.app.modules.user; 2 | 3 | import gt.app.config.Constants; 4 | import gt.app.config.security.AppUserDetails; 5 | import gt.app.domain.AppUser; 6 | import gt.app.domain.LiteUser; 7 | import gt.app.exception.RecordNotFoundException; 8 | import gt.app.modules.email.EmailService; 9 | import gt.app.modules.email.dto.EmailDto; 10 | import gt.app.modules.user.dto.PasswordUpdateDTO; 11 | import gt.app.modules.user.dto.UserProfileUpdateDTO; 12 | import gt.app.modules.user.dto.UserSignUpDTO; 13 | import lombok.RequiredArgsConstructor; 14 | import org.springframework.security.crypto.password.PasswordEncoder; 15 | import org.springframework.stereotype.Service; 16 | 17 | import java.util.Set; 18 | 19 | @Service 20 | @RequiredArgsConstructor 21 | public class UserService { 22 | 23 | private final UserRepository userRepository; 24 | private final PasswordEncoder passwordEncoder; 25 | private final AuthorityService authorityService; 26 | private final EmailService emailService; 27 | 28 | private final LiteUserRepository liteUserRepository; 29 | 30 | public void update(UserProfileUpdateDTO toUpdate, AppUserDetails userDetails) { 31 | LiteUser user = liteUserRepository.findOneByUniqueId(userDetails.getUsername()) 32 | .orElseThrow(() -> new RecordNotFoundException("User", "login", userDetails.getUsername())); 33 | 34 | user.setFirstName(toUpdate.getFirstName()); 35 | user.setLastName(toUpdate.getLastName()); 36 | user.setEmail(toUpdate.getEmail()); 37 | 38 | liteUserRepository.save(user); 39 | } 40 | 41 | public void updatePassword(PasswordUpdateDTO toUpdate, AppUserDetails userDetails) { 42 | LiteUser user = liteUserRepository.findOneByUniqueId(userDetails.getUsername()) 43 | .orElseThrow(() -> new RecordNotFoundException("User", "login", userDetails.getUsername())); 44 | 45 | user.setPassword(passwordEncoder.encode(toUpdate.pwdPlainText())); 46 | liteUserRepository.save(user); 47 | } 48 | 49 | public AppUser create(UserSignUpDTO toCreate) { 50 | 51 | var user = new AppUser(toCreate.getUniqueId(), toCreate.getFirstName(), toCreate.getLastName(), toCreate.getEmail()); 52 | 53 | user.setPassword(passwordEncoder.encode(toCreate.getPwdPlaintext())); 54 | 55 | user.setAuthorities(authorityService.findByNameIn(Constants.ROLE_USER)); 56 | 57 | userRepository.save(user); 58 | 59 | EmailDto dto = EmailDto.of("system@noteapp", Set.of(user.getEmail()), 60 | "NoteApp Account Created!", 61 | "Thanks for signing up."); 62 | 63 | emailService.sendEmail(dto); 64 | 65 | return user; 66 | } 67 | 68 | public void delete(Long id) { 69 | LiteUser author = liteUserRepository.findByIdAndActiveIsTrue(id) 70 | .orElseThrow(() -> new RecordNotFoundException("User", "id", id)); 71 | 72 | author.setActive(Boolean.FALSE); 73 | liteUserRepository.save(author); 74 | } 75 | 76 | public AppUser save(AppUser u) { 77 | return userRepository.save(u); 78 | } 79 | 80 | public boolean existsByUniqueId(String username) { 81 | return userRepository.existsByUniqueId(username); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/gt/app/modules/user/UserSignupValidator.java: -------------------------------------------------------------------------------- 1 | package gt.app.modules.user; 2 | 3 | import gt.app.modules.user.dto.UserSignUpDTO; 4 | import lombok.RequiredArgsConstructor; 5 | import org.apache.commons.lang3.StringUtils; 6 | import org.springframework.stereotype.Component; 7 | import org.springframework.validation.Errors; 8 | import org.springframework.validation.Validator; 9 | 10 | @RequiredArgsConstructor 11 | @Component 12 | public class UserSignupValidator implements Validator { 13 | 14 | final UserRepository userRepository; 15 | 16 | @Override 17 | public boolean supports(Class clazz) { 18 | return UserSignUpDTO.class.equals(clazz); 19 | } 20 | 21 | @Override 22 | public void validate(Object target, Errors errors) { 23 | UserSignUpDTO toCreate = (UserSignUpDTO) target; 24 | 25 | if (StringUtils.containsIgnoreCase(toCreate.getPwdPlaintext(), toCreate.getUniqueId()) || StringUtils.containsIgnoreCase(toCreate.getUniqueId(), toCreate.getPwdPlaintext())) { 26 | errors.rejectValue("pwdPlaintext", "user.weakpwd", "Weak password, choose another"); 27 | } 28 | 29 | if (userRepository.existsByUniqueId(toCreate.getUniqueId())) { 30 | errors.rejectValue("uniqueId", "user.alreadyexists", "Username " + toCreate.getUniqueId() + " already exists"); 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/gt/app/modules/user/dto/PasswordUpdateDTO.java: -------------------------------------------------------------------------------- 1 | package gt.app.modules.user.dto; 2 | 3 | import jakarta.validation.constraints.NotNull; 4 | import jakarta.validation.constraints.Size; 5 | 6 | public record PasswordUpdateDTO(@NotNull @Size(min = 5, max = 50) String pwdPlainText) { 7 | public static PasswordUpdateDTO of() { 8 | return new PasswordUpdateDTO(""); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/gt/app/modules/user/dto/UserDTO.java: -------------------------------------------------------------------------------- 1 | package gt.app.modules.user.dto; 2 | 3 | import java.util.List; 4 | 5 | public record UserDTO(String login, String firstName, String lastName, List authorities) { 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/gt/app/modules/user/dto/UserMapper.java: -------------------------------------------------------------------------------- 1 | package gt.app.modules.user.dto; 2 | 3 | import gt.app.domain.AppUser; 4 | import org.mapstruct.Mapper; 5 | import org.mapstruct.Mapping; 6 | import org.springframework.security.core.GrantedAuthority; 7 | 8 | import java.util.Collection; 9 | import java.util.List; 10 | import java.util.stream.Collectors; 11 | 12 | @Mapper(componentModel = "spring") 13 | public interface UserMapper { 14 | 15 | @Mapping(source = "uniqueId", target = "login") 16 | UserDTO userToUserDto(AppUser user); 17 | 18 | default List mapAuthorities(Collection extends GrantedAuthority> authorities) { 19 | return authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/gt/app/modules/user/dto/UserProfileUpdateDTO.java: -------------------------------------------------------------------------------- 1 | package gt.app.modules.user.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import jakarta.validation.constraints.Email; 8 | import jakarta.validation.constraints.NotNull; 9 | import jakarta.validation.constraints.Size; 10 | 11 | @Data 12 | @AllArgsConstructor 13 | @NoArgsConstructor 14 | public class UserProfileUpdateDTO { 15 | 16 | @Email 17 | private String email; 18 | 19 | @NotNull 20 | @Size(min = 2, max = 30) 21 | private String firstName; 22 | 23 | @Size(max = 30) 24 | private String lastName; 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/gt/app/modules/user/dto/UserSignUpDTO.java: -------------------------------------------------------------------------------- 1 | package gt.app.modules.user.dto; 2 | 3 | import lombok.Data; 4 | import lombok.EqualsAndHashCode; 5 | 6 | import jakarta.validation.constraints.NotNull; 7 | import jakarta.validation.constraints.Size; 8 | 9 | @EqualsAndHashCode(callSuper = true) 10 | @Data 11 | public class UserSignUpDTO extends UserProfileUpdateDTO { 12 | 13 | @NotNull 14 | @Size(min = 5, max = 20) 15 | private String uniqueId; 16 | 17 | @NotNull 18 | @Size(min = 5, max = 50) 19 | private String pwdPlaintext; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/gt/app/web/mvc/DownloadController.java: -------------------------------------------------------------------------------- 1 | package gt.app.web.mvc; 2 | 3 | import gt.app.modules.file.FileDownloadUtil; 4 | import gt.app.modules.file.FileService; 5 | import gt.app.modules.file.ReceivedFileService; 6 | import gt.app.modules.file.RetrievalException; 7 | import jakarta.servlet.http.HttpServletResponse; 8 | import lombok.RequiredArgsConstructor; 9 | import org.springframework.stereotype.Controller; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.PathVariable; 12 | import org.springframework.web.bind.annotation.RequestMapping; 13 | 14 | import java.io.IOException; 15 | import java.util.UUID; 16 | 17 | @Controller 18 | @RequestMapping("/download") 19 | @RequiredArgsConstructor 20 | public class DownloadController { 21 | 22 | final ReceivedFileService receivedFileService; 23 | final FileService fileService; 24 | 25 | @GetMapping("/file/{id}") 26 | //no security check needed 27 | public void downloadFile(@PathVariable UUID id, HttpServletResponse response) throws IOException { 28 | var receivedFile = receivedFileService.findById(id) 29 | .orElseThrow(() -> new RetrievalException("File not found", null)); 30 | 31 | var fileRes = fileService.loadAsResource(receivedFile.getFileGroup(), receivedFile.getStoredName()); 32 | 33 | FileDownloadUtil.downloadFile(response, fileRes.getURL(), receivedFile.getOriginalFileName()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/gt/app/web/mvc/ErrorControllerAdvice.java: -------------------------------------------------------------------------------- 1 | package gt.app.web.mvc; 2 | 3 | import gt.app.modules.file.RetrievalException; 4 | import gt.app.modules.file.StorageException; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.ui.Model; 8 | import org.springframework.web.bind.annotation.ControllerAdvice; 9 | import org.springframework.web.bind.annotation.ExceptionHandler; 10 | import org.springframework.web.bind.annotation.ResponseStatus; 11 | 12 | @ControllerAdvice 13 | @Slf4j 14 | public class ErrorControllerAdvice { 15 | 16 | @ExceptionHandler(Throwable.class) 17 | @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) 18 | public String exception(final Throwable throwable, final Model model) { 19 | log.error("Exception during execution of application", throwable); 20 | String errorMessage = (throwable != null ? throwable.getMessage() : "Unknown error"); 21 | model.addAttribute("errorMessage", errorMessage); 22 | return "error"; 23 | } 24 | 25 | @ExceptionHandler(StorageException.class) 26 | @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) 27 | public String storageException(final StorageException throwable, final Model model) { 28 | log.error("Exception during execution of application", throwable); 29 | model.addAttribute("errorMessage", "Failed to store file"); 30 | return "error"; 31 | } 32 | 33 | @ExceptionHandler(RetrievalException.class) 34 | @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) 35 | public String retrievalException(final RetrievalException throwable, final Model model) { 36 | log.error("Exception during execution of application", throwable); 37 | model.addAttribute("errorMessage", "Failed to read file"); 38 | return "error"; 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/gt/app/web/mvc/IndexController.java: -------------------------------------------------------------------------------- 1 | package gt.app.web.mvc; 2 | 3 | import gt.app.config.security.AppUserDetails; 4 | import gt.app.domain.Note; 5 | import gt.app.modules.note.NoteService; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.data.domain.PageRequest; 8 | import org.springframework.data.domain.Pageable; 9 | import org.springframework.data.domain.Sort; 10 | import org.springframework.security.core.annotation.AuthenticationPrincipal; 11 | import org.springframework.stereotype.Controller; 12 | import org.springframework.ui.Model; 13 | import org.springframework.web.bind.annotation.GetMapping; 14 | 15 | @Controller 16 | @RequiredArgsConstructor 17 | public class IndexController { 18 | 19 | private final NoteService noteService; 20 | 21 | @GetMapping({"/", ""}) 22 | public String index(Model model, Pageable pageable) { 23 | model.addAttribute("greeting", "Hello Spring"); 24 | 25 | model.addAttribute("notes", noteService.readAll(PageRequest.of(0, 20, Sort.by("createdDate").descending()))); 26 | model.addAttribute("note", new Note()); 27 | 28 | return "landing"; 29 | } 30 | 31 | @GetMapping("/admin") 32 | public String adminHome(Model model, @AuthenticationPrincipal AppUserDetails principal) { 33 | model.addAttribute("message", getWelcomeMessage(principal)); 34 | return "admin"; 35 | } 36 | 37 | @GetMapping("/note") 38 | public String userHome(Model model, @AuthenticationPrincipal AppUserDetails principal) { 39 | model.addAttribute("message", getWelcomeMessage(principal)); 40 | model.addAttribute("notes", noteService.readAllByUser(PageRequest.of(0, 20, Sort.by("createdDate").descending()), principal.getId())); 41 | model.addAttribute("note", new Note()); 42 | return "note"; 43 | } 44 | 45 | private String getWelcomeMessage(AppUserDetails principal) { 46 | return "Hello " + principal.getUsername() + "!"; 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/gt/app/web/mvc/NoteController.java: -------------------------------------------------------------------------------- 1 | package gt.app.web.mvc; 2 | 3 | import gt.app.domain.Note; 4 | import gt.app.modules.note.dto.NoteCreateDto; 5 | import gt.app.modules.note.dto.NoteEditDto; 6 | import gt.app.modules.note.NoteService; 7 | import lombok.RequiredArgsConstructor; 8 | import org.springframework.security.access.prepost.PreAuthorize; 9 | import org.springframework.stereotype.Controller; 10 | import org.springframework.ui.Model; 11 | import org.springframework.web.bind.annotation.GetMapping; 12 | import org.springframework.web.bind.annotation.PathVariable; 13 | import org.springframework.web.bind.annotation.PostMapping; 14 | import org.springframework.web.bind.annotation.RequestMapping; 15 | import org.springframework.web.servlet.mvc.support.RedirectAttributes; 16 | 17 | @Controller 18 | @RequestMapping("/note") 19 | @RequiredArgsConstructor 20 | public class NoteController { 21 | 22 | final NoteService noteService; 23 | 24 | @PostMapping("/add") 25 | public String finishAddNote(NoteCreateDto noteDto, RedirectAttributes redirectAttrs) { 26 | //TODO:validate and return to GET:/add on errors 27 | 28 | Note note = noteService.createNote(noteDto); 29 | 30 | redirectAttrs.addFlashAttribute("success", "Note with id " + note.getId() + " is created"); 31 | 32 | return "redirect:/"; 33 | } 34 | 35 | @GetMapping("/delete/{id}") 36 | @PreAuthorize("@permEvaluator.hasAccess(#id, 'Note' )") 37 | public String deleteNote(@PathVariable Long id, RedirectAttributes redirectAttrs) { 38 | noteService.delete(id); 39 | 40 | redirectAttrs.addFlashAttribute("success", "Note with id " + id + " is deleted"); 41 | 42 | return "redirect:/"; 43 | } 44 | 45 | @GetMapping("/edit/{id}") 46 | @PreAuthorize("@permEvaluator.hasAccess(#id, 'Note' )") 47 | public String startEditNote(Model model, @PathVariable Long id) { 48 | model.addAttribute("msg", "Add a new note"); 49 | model.addAttribute("note", noteService.read(id)); 50 | return "note/edit-note"; 51 | } 52 | 53 | @PostMapping("/edit") 54 | @PreAuthorize("@permEvaluator.hasAccess(#noteDto.id, 'Note' )") 55 | public String finishEditNote(Model model, NoteEditDto noteDto, RedirectAttributes redirectAttrs) { 56 | model.addAttribute("msg", "Add a new note"); 57 | //TODO:validate and return to GET:/edit/{id} on errors 58 | 59 | noteService.update(noteDto); 60 | 61 | redirectAttrs.addFlashAttribute("success", "Note with id " + noteDto.id() + " is updated"); 62 | 63 | return "redirect:/note"; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/gt/app/web/mvc/UserController.java: -------------------------------------------------------------------------------- 1 | package gt.app.web.mvc; 2 | 3 | import gt.app.config.security.AppUserDetails; 4 | import gt.app.modules.user.PasswordUpdateValidator; 5 | import gt.app.modules.user.UserService; 6 | import gt.app.modules.user.UserSignupValidator; 7 | import gt.app.modules.user.dto.PasswordUpdateDTO; 8 | import gt.app.modules.user.dto.UserProfileUpdateDTO; 9 | import gt.app.modules.user.dto.UserSignUpDTO; 10 | import jakarta.validation.Valid; 11 | import lombok.RequiredArgsConstructor; 12 | import org.springframework.security.core.annotation.AuthenticationPrincipal; 13 | import org.springframework.stereotype.Controller; 14 | import org.springframework.ui.Model; 15 | import org.springframework.validation.BindingResult; 16 | import org.springframework.web.bind.annotation.GetMapping; 17 | import org.springframework.web.bind.annotation.ModelAttribute; 18 | import org.springframework.web.bind.annotation.PostMapping; 19 | import org.springframework.web.servlet.mvc.support.RedirectAttributes; 20 | 21 | @Controller 22 | @RequiredArgsConstructor 23 | public class UserController { 24 | 25 | private final UserService userService; 26 | private final UserSignupValidator userSignupValidator; 27 | private final PasswordUpdateValidator passwordUpdateValidator; 28 | 29 | @GetMapping(value = "/signup") 30 | public String register(Model model) { 31 | model.addAttribute("user", new UserSignUpDTO()); 32 | return "user/signup"; 33 | } 34 | 35 | @PostMapping(value = "/signup") 36 | public String register(@Valid @ModelAttribute UserSignUpDTO user, BindingResult bindingResult, 37 | RedirectAttributes redirectAttrs) { 38 | //do custom validation along with the BeanValidation 39 | userSignupValidator.validate(user, bindingResult); 40 | 41 | if (bindingResult.hasErrors()) { 42 | return "user/signup"; 43 | } 44 | 45 | userService.create(user); 46 | 47 | redirectAttrs.addFlashAttribute("success", "User " + user.getUniqueId() + " is created"); 48 | 49 | return "redirect:/"; 50 | } 51 | 52 | @GetMapping(value = "/profile") 53 | public String updateProfile(Model model, @AuthenticationPrincipal AppUserDetails loggedInUserDtl) { 54 | model.addAttribute("user", new UserProfileUpdateDTO(loggedInUserDtl.getEmail(), loggedInUserDtl.getFirstName(), loggedInUserDtl.getLastName())); 55 | return "user/profile"; 56 | } 57 | 58 | @PostMapping(value = "/profile") 59 | public String updateProfile(@Valid @ModelAttribute UserProfileUpdateDTO user, BindingResult bindingResult, 60 | @AuthenticationPrincipal AppUserDetails loggedInUserDtl, RedirectAttributes redirectAttrs) { 61 | if (bindingResult.hasErrors()) { 62 | return "user/profile"; 63 | } 64 | 65 | userService.update(user, loggedInUserDtl); 66 | 67 | redirectAttrs.addFlashAttribute("success", "Profile updated successfully"); 68 | 69 | return "redirect:/"; //self 70 | } 71 | 72 | @GetMapping(value = "/password") 73 | public String updatePassword(Model model) { 74 | model.addAttribute("user", PasswordUpdateDTO.of()); 75 | return "user/password"; 76 | } 77 | 78 | @PostMapping(value = "/password") 79 | public String updatePassword(@Valid @ModelAttribute("user") PasswordUpdateDTO reqDto, BindingResult bindingResult, 80 | @AuthenticationPrincipal AppUserDetails loggedInUserDtl, RedirectAttributes redirectAttrs) { 81 | //do custom validation along with the BeanValidation 82 | passwordUpdateValidator.validate(reqDto, bindingResult, loggedInUserDtl); 83 | 84 | if (bindingResult.hasErrors()) { 85 | return "user/password"; 86 | } 87 | 88 | userService.updatePassword(reqDto, loggedInUserDtl); 89 | 90 | redirectAttrs.addFlashAttribute("success", "Password updated successfully"); 91 | 92 | return "redirect:/"; 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/gt/app/web/rest/HelloResource.java: -------------------------------------------------------------------------------- 1 | package gt.app.web.rest; 2 | 3 | import gt.app.config.Constants; 4 | import gt.app.modules.email.dto.EmailDto; 5 | import gt.app.modules.email.EmailService; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.context.annotation.Profile; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.PostMapping; 12 | import org.springframework.web.bind.annotation.RequestBody; 13 | import org.springframework.web.bind.annotation.RestController; 14 | 15 | import jakarta.validation.Valid; 16 | import jakarta.validation.constraints.NotNull; 17 | import java.util.Map; 18 | 19 | @RestController 20 | @RequiredArgsConstructor 21 | @Slf4j 22 | @Profile({Constants.SPRING_PROFILE_DEVELOPMENT, Constants.SPRING_PROFILE_TEST}) 23 | public class HelloResource { 24 | private final EmailService emailService; 25 | 26 | @GetMapping("/debug/hello") 27 | public Map sayHello() { 28 | return Map.of("hello", "world"); 29 | } 30 | 31 | @PostMapping("/debug/sendEmail") 32 | public ResponseEntity sendEmailWithAttachments(@RequestBody @Valid @NotNull EmailDto email) { 33 | log.debug("Sending email ..."); 34 | 35 | emailService.sendEmail(email); 36 | 37 | return ResponseEntity.ok().build(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/gt/app/web/rest/UserResource.java: -------------------------------------------------------------------------------- 1 | package gt.app.web.rest; 2 | 3 | import gt.app.config.security.SecurityUtils; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.security.core.userdetails.User; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | @RestController 11 | @RequestMapping("/api") 12 | @RequiredArgsConstructor 13 | public class UserResource { 14 | 15 | @GetMapping("/account") 16 | public User getAccount() { 17 | return SecurityUtils.getCurrentUserDetails(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/resources/application-default.yml: -------------------------------------------------------------------------------- 1 | # 'default' profile gets selected when we do not specify any profile 2 | spring: 3 | profiles: 4 | active: dev 5 | -------------------------------------------------------------------------------- /src/main/resources/application-dev.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | thymeleaf: 3 | cache: false 4 | prefix: file:src/main/resources/templates/ #directly serve from src folder instead of target 5 | web: 6 | resources: 7 | static-locations: 8 | - file:src/main/resources/static/ #directly serve from src folder instead of target 9 | - classpath:/META-INF/resources/ 10 | - classpath:/resources/ 11 | - classpath:/static/ 12 | - classpath:/public/ 13 | cache: 14 | period: 0 15 | mvc: 16 | static-path-pattern: /static/** 17 | jpa: 18 | show-sql: false 19 | datasource: 20 | url: jdbc:h2:mem:testdb 21 | h2: #dev uses H2 and 'emailhog' docker container from docker-compose 22 | console: 23 | enabled: true #Access from http://localhost:8080/h2-console/ 24 | docker: 25 | compose: 26 | profiles: 27 | active: mailHog 28 | -------------------------------------------------------------------------------- /src/main/resources/application-docker.yml: -------------------------------------------------------------------------------- 1 | # meant to be used locally with docker-compose 2 | #NOTE: do not run docker-compose separately when running app with this profile. kill the containers if they are running 3 | 4 | # if you run the app with this `docker` spring profile, `all` docker-compose profile will be selected 5 | # which will initialize MySQL. Also the jdbc url will be automatically created based on the docker-compose.yml file 6 | spring: 7 | docker: 8 | compose: 9 | profiles: 10 | active: 11 | - all 12 | 13 | spring.jpa.hibernate.ddl-auto: update 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/main/resources/application-prod.yml: -------------------------------------------------------------------------------- 1 | #NOTE: do not run docker-compose separately when running app with this profile. kill the containers if they are running 2 | 3 | # if you run the app with this `prod` spring profile, `all` docker-compose profile will be selected 4 | # which will initialize MySQL. Also the jdbc url will be automatically created based on the docker-compose.yml file 5 | spring: 6 | docker: 7 | compose: 8 | profiles.active: 9 | - all 10 | 11 | #spring.jpa.hibernate.ddl-auto: none #should use liquibase//TODO: 12 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: Seed App - An example of professional spring project 4 | profiles: 5 | # The commented value for `active` can be replaced with valid spring profiles to load. 6 | # Otherwise, it will be filled in by maven when building the WAR file 7 | # Either way, it can be overridden by `--spring.profiles.active` value passed in the commandline or `-Dspring.profiles.active` set in `JAVA_OPTS` 8 | active: '@spring.profiles.active@' 9 | jpa: 10 | show-sql: false 11 | open-in-view: false 12 | properties: 13 | hibernate.globally_quoted_identifiers: true 14 | mail: 15 | host: localhost 16 | port: ${MAILHOG_PORT:1025} 17 | username: 18 | password: 19 | main: 20 | lazy-initialization: false 21 | threads: 22 | virtual: 23 | enabled: true 24 | server: 25 | port: 8080 26 | 27 | logging.level: 28 | org.springframework.web: INFO 29 | org.springframework.security: INFO 30 | org.springframework.security.web: INFO 31 | org.hibernate: INFO 32 | ROOT: INFO 33 | gt: DEBUG 34 | 35 | 36 | wro4j: 37 | jmx-enabled: false 38 | debug: true 39 | manager-factory: 40 | pre-processors: removeSourceMaps, cssUrlRewriting, cssImport, cssMinJawr, semicolonAppender, jsMin 41 | filter-url: /wro4j # this is the default, needs to be used in secConfig and the htmls 42 | 43 | 44 | # custom properties for this project 45 | app-properties: 46 | file-storage: 47 | upload-folder: ${java.io.tmpdir} 48 | 49 | 50 | spring-doc: 51 | show-actuator: true 52 | 53 | -------------------------------------------------------------------------------- /src/main/resources/checkstyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 32 | 33 | 34 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | -------------------------------------------------------------------------------- /src/main/resources/static/css/app.css: -------------------------------------------------------------------------------- 1 | 2 | ::-webkit-scrollbar { 3 | width: 10px; 4 | } 5 | 6 | ::-webkit-scrollbar-track { 7 | background: #f1f1f1; 8 | } 9 | 10 | ::-webkit-scrollbar-thumb { 11 | background: #acacac; 12 | } 13 | 14 | ::-webkit-scrollbar-thumb:hover { 15 | background: #555; 16 | } 17 | 18 | 19 | .hidden { 20 | display: none; 21 | } 22 | 23 | .note-box { 24 | margin-bottom: 20px !important; 25 | margin-left: 0 !important; 26 | margin-right: 0 !important; 27 | } 28 | 29 | .error { 30 | color: red; 31 | font-size: smaller; 32 | } 33 | -------------------------------------------------------------------------------- /src/main/resources/static/css/app2.css: -------------------------------------------------------------------------------- 1 | .warn { 2 | color: yellow; 3 | font-size: smaller; 4 | } 5 | -------------------------------------------------------------------------------- /src/main/resources/static/img/male-coat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gtiwari333/spring-boot-blog-app/aa3d93f6e8e22b52bc7506a89de73bbbd5fdd26f/src/main/resources/static/img/male-coat.png -------------------------------------------------------------------------------- /src/main/resources/static/img/male-tshirt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gtiwari333/spring-boot-blog-app/aa3d93f6e8e22b52bc7506a89de73bbbd5fdd26f/src/main/resources/static/img/male-tshirt.png -------------------------------------------------------------------------------- /src/main/resources/static/js/app.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | //adding spaces to test minify 3 | 4 | 5 | jQuery(document).ready(function () { 6 | 7 | var abc = "DFDF"; 8 | 9 | 10 | 11 | 12 | console.log(abc); 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | console.log(abc); 25 | 26 | 27 | console.log(abc); 28 | 29 | 30 | }); 31 | 32 | })(); 33 | -------------------------------------------------------------------------------- /src/main/resources/static/js/custom.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | //adding spaces to test minify 3 | 4 | 5 | jQuery(document).ready(function () { 6 | 7 | var abc = "Custom"; 8 | 9 | 10 | 11 | 12 | console.log(abc); 13 | 14 | 15 | 16 | }); 17 | 18 | })(); 19 | -------------------------------------------------------------------------------- /src/main/resources/templates/_fragments/footer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/main/resources/templates/_fragments/header.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Title ... 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | Note App 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | Notes's Notes 36 | 37 | 38 | 39 | 40 | Login 41 | 42 | 43 | 44 | 45 | 46 | Signup 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | USER_NAME 55 | 56 | 57 | 58 | Change Password 59 | 60 | 61 | Profile 62 | 63 | 64 | Logout 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /src/main/resources/templates/admin.html: -------------------------------------------------------------------------------- 1 | TODO: 2 | -------------------------------------------------------------------------------- /src/main/resources/templates/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Opps! 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | Error java.lang.NullPointerException 24 | Back to Home Page 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/main/resources/templates/landing.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Note App - HOME 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | You have been signed out 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 32 | 33 | 34 | Post Note 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | No notes available. 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/main/resources/templates/note.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Notes 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | Post Note 33 | 34 | 35 | 36 | 37 | Notes 38 | 39 | 40 | 41 | Title 42 | Content 43 | Files 44 | Created On 45 | Action 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | Edit 61 | 62 | 63 | 64 | 65 | Delete 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /src/main/resources/templates/note/_notes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Title 12 | 14 | 15 | 16 | 17 | 18 | 19 | Content 20 | 22 | 23 | 24 | 25 | 26 | File 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | Attachments: 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/main/resources/templates/note/edit-note.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Edit Note 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | Update Note 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/main/resources/templates/user/password.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | Note App - Signup 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | Update Password 19 | 20 | 21 | New Password 22 | 25 | 27 | 28 | 29 | 30 | Update 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/main/resources/templates/user/profile.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | Note App - Signup 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | User Profile 19 | 20 | 21 | First name 22 | 25 | 26 | 27 | 28 | 29 | Last name 30 | 31 | 32 | 33 | 34 | 35 | 36 | Email 37 | 39 | 40 | 41 | 42 | 43 | Update 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/main/resources/templates/user/signup.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | Note App - Signup 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | You are already logged in. 20 | 21 | 22 | 23 | 24 | User Registration 25 | 26 | 27 | First name 28 | 31 | 32 | 33 | 34 | 35 | Last name 36 | 37 | 38 | 39 | 40 | 41 | 42 | Email 43 | 45 | 46 | 47 | 48 | 49 | Username 50 | 51 | 52 | @ 53 | 54 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | Password 63 | 66 | 68 | 69 | 70 | 71 | Register 72 | 73 | Already registered? 74 | 75 | Login 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /src/main/resources/wro.properties: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/main/resources/wro.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | classpath:static/js/app.js 7 | classpath:static/css/app.css 8 | classpath:static/css/app2.css 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/test/groovy/gt/app/DataDrivenSpec.groovy: -------------------------------------------------------------------------------- 1 | package gt.app 2 | 3 | import spock.lang.Specification 4 | 5 | class DataDrivenSpec extends Specification { 6 | def "maximum of two numbers"() { 7 | expect: 8 | Math.max(a, b) == c 9 | 10 | where: 11 | a << [3, 5, 9] 12 | b << [7, 4, 9] 13 | c << [7, 5, 9] 14 | } 15 | 16 | def "minimum of #a and #b is #c"() { 17 | expect: 18 | Math.min(a, b) == c 19 | 20 | where: 21 | a | b || c 22 | 3 | 7 || 3 23 | 5 | 4 || 4 24 | 9 | 9 || 9 25 | } 26 | 27 | def "#person.name is a #sex.toLowerCase() person"() { 28 | expect: 29 | person.getSex() == sex 30 | 31 | where: 32 | person || sex 33 | new Person(name: "Fred") || "Male" 34 | new Person(name: "Wilma") || "Female" 35 | } 36 | 37 | static class Person { 38 | String name 39 | 40 | String getSex() { 41 | name == "Fred" ? "Male" : "Female" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/test/groovy/gt/app/SpockExSpec.groovy: -------------------------------------------------------------------------------- 1 | package gt.app 2 | 3 | import spock.lang.Specification 4 | 5 | class SpockExSpec extends Specification { 6 | 7 | def "length of Spock's and his friends' names"() { 8 | expect: 9 | name.size() == length 10 | 11 | where: 12 | name | length 13 | "Spock" | 5 14 | "Kirk" | 4 15 | "Scotty" | 6 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/test/groovy/gt/app/SpringContextSpec.groovy: -------------------------------------------------------------------------------- 1 | package gt.app 2 | 3 | import gt.app.config.Constants 4 | import gt.app.web.rest.HelloResource 5 | import org.springframework.beans.factory.annotation.Autowired 6 | import org.springframework.boot.test.context.SpringBootTest 7 | import org.springframework.test.context.ActiveProfiles 8 | import spock.lang.Specification 9 | 10 | @SpringBootTest 11 | @ActiveProfiles(Constants.SPRING_PROFILE_TEST) 12 | class SpringContextSpec extends Specification { 13 | @Autowired 14 | private HelloResource webController 15 | 16 | def "when context is loaded then all expected beans are created"() { 17 | expect: "the WebController is created" 18 | 1 == 1 19 | webController 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/test/groovy/gt/app/modules/user/AppUserServiceSpec.groovy: -------------------------------------------------------------------------------- 1 | package gt.app.modules.user 2 | 3 | import gt.app.config.Constants 4 | import gt.app.domain.AppUser 5 | import gt.app.modules.email.EmailService 6 | import gt.app.modules.email.dto.EmailDto 7 | import gt.app.modules.user.dto.UserSignUpDTO 8 | import org.springframework.security.crypto.password.NoOpPasswordEncoder 9 | import org.springframework.security.crypto.password.PasswordEncoder 10 | import spock.lang.Specification 11 | 12 | class AppUserServiceSpec extends Specification { 13 | 14 | UserRepository userRepository 15 | LiteUserRepository liteUserRepository 16 | PasswordEncoder passwordEncoder 17 | AuthorityService authorityService 18 | EmailService emailService 19 | 20 | UserService userService 21 | 22 | def setup() { 23 | userRepository = Mock() 24 | passwordEncoder = NoOpPasswordEncoder.getInstance() 25 | authorityService = Mock() 26 | emailService = Mock() 27 | userService = new UserService(userRepository, passwordEncoder, authorityService, emailService, liteUserRepository) 28 | } 29 | 30 | def 'create user'() { 31 | given: 32 | def toCreate = new UserSignUpDTO(uniqueId: 'U01', pwdPlaintext: 'pass', lastName: 'last1', firstName: 'first', email: 'gg@email') 33 | 34 | when: 35 | AppUser user = userService.create(toCreate) 36 | 37 | then: 38 | user.password == toCreate.pwdPlaintext //noop encoder 39 | user.uniqueId == toCreate.uniqueId 40 | user.email == toCreate.email 41 | 1 * authorityService.findByNameIn(Constants.ROLE_USER) 42 | 1 * userRepository.save(_) 43 | 1 * emailService.sendEmail(_) >> { 44 | //argument capture and assertions 45 | EmailDto dto -> 46 | assert dto.to()[0] == toCreate.email 47 | assert dto.cc().size() == 0 48 | assert dto.bcc().size() == 0 49 | assert dto.from() == "system@noteapp" 50 | assert dto.subject() == "NoteApp Account Created!" 51 | assert dto.content() == "Thanks for signing up." 52 | } 53 | 54 | 0 * _ 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/test/java/gt/app/ApplicationStartupTest.java: -------------------------------------------------------------------------------- 1 | package gt.app; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.net.UnknownHostException; 6 | 7 | class ApplicationStartupTest { 8 | 9 | @Test 10 | void applicationCanBeStartedWithDefaultConfigByRunningMainMethod() throws UnknownHostException { 11 | /* 12 | this ensures that the 'Application' can be "Simply" run from IDE without doing any config change(the yml files or vm arg). 13 | */ 14 | Application.main(new String[]{}); 15 | } 16 | 17 | } 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/test/java/gt/app/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package gt.app; 2 | 3 | import gt.app.config.Constants; 4 | import gt.app.web.rest.HelloResource; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.junit.jupiter.api.Test; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.test.context.ActiveProfiles; 10 | 11 | import static org.junit.jupiter.api.Assertions.assertNotNull; 12 | import static org.junit.jupiter.api.Assertions.assertTrue; 13 | 14 | @SpringBootTest 15 | @ActiveProfiles(Constants.SPRING_PROFILE_TEST) 16 | class ApplicationTest { 17 | 18 | @Autowired 19 | private HelloResource webController; 20 | @Test 21 | void contextLoads() { 22 | assertTrue(true, "Context loads !!!"); 23 | assertNotNull(webController); 24 | } 25 | 26 | } 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/test/java/gt/app/arch/ArchitectureTest.java: -------------------------------------------------------------------------------- 1 | package gt.app.arch; 2 | 3 | import com.tngtech.archunit.core.domain.JavaClasses; 4 | import com.tngtech.archunit.core.importer.ClassFileImporter; 5 | import com.tngtech.archunit.core.importer.ImportOption; 6 | import org.junit.jupiter.api.BeforeAll; 7 | 8 | abstract class ArchitectureTest { 9 | static final String DOMAIN_LAYER_PACKAGES = "gt.app.domain.."; 10 | static final String SERVICE_LAYER_PACKAGES = "gt.app.modules.."; 11 | static final String WEB_LAYER_CLASSES = "gt.app.web.."; 12 | static final String CONFIG_PACKAGE = "gt.app.config.."; 13 | 14 | static JavaClasses classes; 15 | 16 | @BeforeAll 17 | static void setUp() { 18 | classes = new ClassFileImporter() 19 | .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS) 20 | .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_ARCHIVES) 21 | .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_JARS) 22 | .importPackages("gt.app"); 23 | } 24 | 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/test/java/gt/app/arch/GeneralCodingRulesTest.java: -------------------------------------------------------------------------------- 1 | package gt.app.arch; 2 | 3 | import com.tngtech.archunit.core.domain.JavaClass; 4 | import com.tngtech.archunit.lang.ArchCondition; 5 | import com.tngtech.archunit.lang.ArchRule; 6 | import com.tngtech.archunit.lang.syntax.ArchRuleDefinition; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import java.sql.Date; 10 | import java.time.*; 11 | 12 | import static com.tngtech.archunit.core.domain.JavaClass.Predicates.resideInAnyPackage; 13 | import static com.tngtech.archunit.lang.conditions.ArchConditions.dependOnClassesThat; 14 | import static com.tngtech.archunit.library.GeneralCodingRules.*; 15 | 16 | class GeneralCodingRulesTest extends ArchitectureTest { 17 | 18 | 19 | private static final ArchCondition USE_PACKAGES_FROM_TRANSITIVE_DEPENDENCIES = 20 | dependOnClassesThat(resideInAnyPackage("org.unbescape", "org.skyscreamer", "org.h2", 21 | "com.jcraft", "com.zaxxer", "org.xbill", "io.netty", 22 | "net.minidev", "org.attoparser", "org.checkerframework", "org.brotli", "org.objenesis", 23 | "org.opentest4j", "org.rauschig", 24 | "org.webjars", "org.littleshoot", "org.xmlunit", 25 | "org.jvnet", "org.mozilla", "antlr")) 26 | .as("classes from transitive dependencies should not be used"); 27 | 28 | private static final ArchCondition USE_INTERNAL_PACKAGE = 29 | dependOnClassesThat(resideInAnyPackage("javafx", "java.beans", "java.rmi", "com.oracle", "jdk", "sun", "com.sun")) 30 | .as("java/sun/oracle internal packages should not be used"); 31 | 32 | @Test 33 | void noClassesShouldUseStandardStreams() { 34 | 35 | ArchRule rule = ArchRuleDefinition.noClasses() 36 | .should(ACCESS_STANDARD_STREAMS); 37 | rule.check(classes); 38 | } 39 | 40 | @Test 41 | void noClassesShouldThrowGenericExceptions() { 42 | ArchRule rule = ArchRuleDefinition.noClasses() 43 | .should(THROW_GENERIC_EXCEPTIONS); 44 | rule.check(classes); 45 | } 46 | 47 | @Test 48 | void noClassesShouldUseStandardLogging() { 49 | ArchRule rule = ArchRuleDefinition.noClasses() 50 | .should(USE_JAVA_UTIL_LOGGING); 51 | rule.check(classes); 52 | } 53 | 54 | @Test 55 | void noClassesShouldUseJodaTime() { 56 | ArchRule rule = ArchRuleDefinition.noClasses() 57 | .should(USE_JODATIME); 58 | 59 | rule.check(classes); 60 | } 61 | 62 | @Test 63 | void noClassesShouldUseInternalPackages() { 64 | ArchRule rule = ArchRuleDefinition.noClasses() 65 | .should(USE_INTERNAL_PACKAGE); 66 | 67 | rule.check(classes); 68 | } 69 | 70 | @Test 71 | void noClassesShouldUseClassesFromTransitiveDependency() { 72 | ArchRule rule = ArchRuleDefinition.noClasses() 73 | .should(USE_PACKAGES_FROM_TRANSITIVE_DEPENDENCIES); 74 | 75 | rule.check(classes); 76 | } 77 | 78 | 79 | @Test 80 | void noClassShouldUseDateToString() { 81 | ArchRule rule = ArchRuleDefinition.noClasses() 82 | .should().callMethod(LocalDate.class, "toString") 83 | .orShould().callMethod(LocalDateTime.class, "toString") 84 | .orShould().callMethod(LocalTime.class, "toString") 85 | .orShould().callMethod(ZonedDateTime.class, "toString") 86 | .orShould().callMethod(Instant.class, "toString") 87 | .orShould().callMethod(Date.class, "toString") 88 | .because("You must use date formatter to format date objects"); 89 | 90 | rule.check(classes); 91 | } 92 | 93 | @Test 94 | void noClassesShouldHaveMainMethods() { 95 | ArchRule rule = ArchRuleDefinition.methods() 96 | .that().areDeclaredInClassesThat().resideInAPackage("gt.app..") 97 | .and().areDeclaredInClassesThat().doNotHaveFullyQualifiedName("gt.app.Application") 98 | .and().arePublic() 99 | .and().areStatic() 100 | .should().notHaveName("main") 101 | .because("Write unit tests with assertions instead of writing main classes to test stuff!!!"); 102 | 103 | rule.check(classes); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/test/java/gt/app/arch/SpringCodingRulesTest.java: -------------------------------------------------------------------------------- 1 | package gt.app.arch; 2 | 3 | import com.tngtech.archunit.lang.ArchRule; 4 | import com.tngtech.archunit.lang.syntax.ArchRuleDefinition; 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.context.properties.ConfigurationProperties; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.stereotype.Controller; 11 | import org.springframework.stereotype.Repository; 12 | import org.springframework.stereotype.Service; 13 | import org.springframework.web.bind.annotation.*; 14 | 15 | import jakarta.annotation.PostConstruct; 16 | import jakarta.persistence.Entity; 17 | 18 | class SpringCodingRulesTest extends ArchitectureTest { 19 | 20 | @Test 21 | void springSingletonComponentsShouldOnlyHaveFinalFields() { 22 | ArchRule rule = ArchRuleDefinition.classes() 23 | .that().areAnnotatedWith(Service.class) 24 | .or().areAnnotatedWith(Component.class) 25 | .or().areAnnotatedWith(ConfigurationProperties.class) 26 | .or().areAnnotatedWith(Controller.class) 27 | .or().areAnnotatedWith(RestController.class) 28 | .or().areAnnotatedWith(Repository.class) 29 | .should().haveOnlyFinalFields(); 30 | 31 | rule.check(classes); 32 | } 33 | 34 | @Test 35 | void springFieldDependencyInjectionShouldNotBeUsed() { 36 | ArchRule rule = ArchRuleDefinition.noFields() 37 | .should().beAnnotatedWith(Autowired.class); 38 | 39 | rule.check(classes); 40 | } 41 | 42 | @Test 43 | void springComponentInteraction() { 44 | 45 | ArchRule rule = 46 | ArchRuleDefinition.classes() 47 | .that().resideInAPackage("..service..") 48 | .should().onlyBeAccessed() 49 | .byAnyPackage("..service..", 50 | "..web..", 51 | "..security..", 52 | "..config.." 53 | ); 54 | 55 | rule.check(classes); 56 | } 57 | 58 | 59 | @Test 60 | void springRepositoryInteraction() { 61 | 62 | ArchRule rule = 63 | ArchRuleDefinition.classes() 64 | .that().resideInAPackage("..repository..") 65 | .should().onlyBeAccessed() 66 | .byAnyPackage( 67 | "..modules.." 68 | ); 69 | 70 | rule.check(classes); 71 | } 72 | 73 | @Test 74 | void domainClassesShouldOnlyDependOnDomainOrStdLibClasses() { 75 | ArchRule rule = ArchRuleDefinition.classes() 76 | .that().resideInAPackage(DOMAIN_LAYER_PACKAGES) 77 | .and().areAnnotatedWith(Entity.class) 78 | .should().onlyDependOnClassesThat().resideInAnyPackage( 79 | DOMAIN_LAYER_PACKAGES, "java..", "lombok..", "jakarta..", "", 80 | "com.fasterxml.jackson..", "org.hibernate.annotations", 81 | "org.hibernate.engine.spi..", "org.hibernate.bytecode.enhance..", "org.hibernate.collection.spi..", 82 | "org.hibernate.internal.util.collections..", //required for native image 83 | "org.apache.commons.lang3..", "org.springframework.security.core.." 84 | ); 85 | rule.check(classes); 86 | } 87 | 88 | @Test 89 | void controllerClassesShouldBeAnnotatedWithControllerOrRestControllerAnnotation() { 90 | ArchRule rule = ArchRuleDefinition.classes() 91 | .that().haveSimpleNameEndingWith("Controller") 92 | .or().haveSimpleNameEndingWith("Resource") 93 | .should().beAnnotatedWith(Controller.class) 94 | .orShould().beAnnotatedWith(RestController.class); 95 | 96 | rule.check(classes); 97 | } 98 | 99 | @Test 100 | void noClassesWithControllerOrRestControllerAnnotationShouldResideOutsideOfPrimaryAdaptersPackages() { 101 | ArchRule rule = ArchRuleDefinition.noClasses() 102 | .that().areAnnotatedWith(Controller.class) 103 | .or().areAnnotatedWith(RestController.class) 104 | .should().resideOutsideOfPackage(WEB_LAYER_CLASSES); 105 | 106 | rule.check(classes); 107 | } 108 | 109 | @Test 110 | void controllerClassesShouldNotDependOnEachOther() { 111 | ArchRule rule = ArchRuleDefinition.noClasses() 112 | .that().areAnnotatedWith(Controller.class) 113 | .or().areAnnotatedWith(RestController.class) 114 | .should().dependOnClassesThat() 115 | .resideInAPackage(WEB_LAYER_CLASSES); 116 | 117 | rule.check(classes); 118 | } 119 | 120 | @Test 121 | void publicRestControllerMethodsShouldBeAnnotatedWithARequestMapping() { 122 | ArchRule rule = ArchRuleDefinition.methods() 123 | .that().arePublic() 124 | .and().areDeclaredInClassesThat().areAnnotatedWith(RestController.class) 125 | .should().beAnnotatedWith(RequestMapping.class) 126 | .orShould().beAnnotatedWith(GetMapping.class) 127 | .orShould().beAnnotatedWith(PostMapping.class) 128 | .orShould().beAnnotatedWith(PutMapping.class) 129 | .orShould().beAnnotatedWith(DeleteMapping.class); 130 | 131 | rule.check(classes); 132 | } 133 | 134 | @Test 135 | void publicControllerMethodsShouldBeAnnotatedWithARequestMapping() { 136 | ArchRule rule = ArchRuleDefinition.methods() 137 | .that().arePublic() 138 | .and().areDeclaredInClassesThat().areAnnotatedWith(Controller.class) 139 | .should().beAnnotatedWith(RequestMapping.class) 140 | .orShould().beAnnotatedWith(GetMapping.class) 141 | .orShould().beAnnotatedWith(PostMapping.class) 142 | .orShould().beAnnotatedWith(PutMapping.class) 143 | .orShould().beAnnotatedWith(DeleteMapping.class); 144 | 145 | rule.check(classes); 146 | } 147 | 148 | 149 | @Test 150 | void noClassShouldHaveMethodAnnotatedWithPostConstruct() { 151 | ArchRule rule = ArchRuleDefinition.methods() 152 | .should().notBeAnnotatedWith(PostConstruct.class) 153 | .because("You need to implement InitializingBean and move your @PostConstruct logic into afterPropertiesSet() so that Spring handles bean initialization better "); 154 | 155 | rule.check(classes); 156 | } 157 | 158 | 159 | @Test 160 | void noClassesWithEntityAnnotationShouldResideOutsideOfDomainPackage() { 161 | ArchRule rule = ArchRuleDefinition.noClasses() 162 | .that().areAnnotatedWith(Entity.class) 163 | .should().resideOutsideOfPackage(DOMAIN_LAYER_PACKAGES); 164 | rule.check(classes); 165 | } 166 | 167 | 168 | @Test 169 | void serviceImplClassesShouldBeAnnotatedWithServiceAnnotation() { 170 | ArchRule rule = ArchRuleDefinition.classes() 171 | .that().haveSimpleNameEndingWith("ServiceImpl") 172 | .should().beAnnotatedWith(Service.class); 173 | 174 | rule.check(classes); 175 | } 176 | 177 | @Test 178 | void serviceClassesShouldBeAnnotatedWithServiceAnnotation() { 179 | ArchRule rule = ArchRuleDefinition.classes() 180 | .that().haveSimpleNameEndingWith("Service") 181 | .and().areNotInterfaces() 182 | .should().beAnnotatedWith(Service.class); 183 | 184 | rule.check(classes); 185 | } 186 | 187 | @Test 188 | void noServiceClassesShouldResideOutsideDesignatedPackages() { 189 | ArchRule rule = ArchRuleDefinition.noClasses() 190 | .that().haveSimpleNameEndingWith("Service") 191 | .or().areAnnotatedWith(Service.class) 192 | .should().resideOutsideOfPackages(SERVICE_LAYER_PACKAGES); 193 | 194 | rule.check(classes); 195 | 196 | } 197 | 198 | 199 | @Test 200 | void noControllerClassesShouldResideOutsideDesignatedPackages() { 201 | ArchRule rule = ArchRuleDefinition.noClasses() 202 | .that().haveSimpleNameEndingWith("Controller") 203 | .or().areAnnotatedWith(Controller.class) 204 | .or().areAnnotatedWith(RestController.class) 205 | .should().resideOutsideOfPackages(WEB_LAYER_CLASSES); 206 | 207 | rule.check(classes); 208 | 209 | } 210 | 211 | @Test 212 | void noClassesWithConfigurationAnnotationShouldResideOutsideOfConfigPackages() { 213 | ArchRule rule = ArchRuleDefinition.noClasses() 214 | .that().areAnnotatedWith(Configuration.class) 215 | .should().resideOutsideOfPackage(CONFIG_PACKAGE); 216 | rule.check(classes); 217 | } 218 | 219 | 220 | } 221 | -------------------------------------------------------------------------------- /src/test/java/gt/app/config/DBMetadataReader.java: -------------------------------------------------------------------------------- 1 | package gt.app.config; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.hibernate.boot.Metadata; 6 | import org.hibernate.mapping.*; 7 | import org.springframework.beans.factory.InitializingBean; 8 | import org.springframework.stereotype.Component; 9 | 10 | import jakarta.persistence.EntityManager; 11 | import java.util.Iterator; 12 | 13 | //@Component disabled since class is not actually used and was added for my blog 14 | @RequiredArgsConstructor 15 | @Slf4j 16 | public class DBMetadataReader implements InitializingBean { 17 | final EntityManager em; 18 | 19 | @Override 20 | public void afterPropertiesSet() { 21 | 22 | Metadata metadata = MetadataExtractorIntegrator.INSTANCE.getMetadata(); 23 | 24 | //Collection tables 25 | for (Collection c : metadata.getCollectionBindings()) { 26 | log.info("Collection table: {}", c.getCollectionTable().getQualifiedTableName()); 27 | for (Column property : c.getCollectionTable().getColumns()) { 28 | log.info(" {} {} ", property.getName(), property.getSqlType()); 29 | } 30 | } 31 | 32 | //all entities 33 | for (PersistentClass pc : metadata.getEntityBindings()) { 34 | Table table = pc.getTable(); 35 | 36 | log.info("Entity: {} - {}", pc.getClassName(), table.getName()); 37 | 38 | KeyValue identifier = pc.getIdentifier(); 39 | 40 | //PK 41 | for (Column column : identifier.getColumns()) { 42 | log.info(" PK: {} {}", column.getName(), column.getSqlType()); 43 | } 44 | 45 | //property/columns 46 | for (Property property : pc.getProperties()) { 47 | for (Column column : property.getColumns()) { 48 | log.info(" {} {}", column.getName(), column.getSqlType()); 49 | } 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/test/java/gt/app/config/HibernateConfig.java: -------------------------------------------------------------------------------- 1 | package gt.app.config; 2 | 3 | import org.hibernate.jpa.boot.spi.IntegratorProvider; 4 | import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | @Configuration 11 | public class HibernateConfig implements HibernatePropertiesCustomizer { 12 | @Override 13 | public void customize(Map hibernateProps) { 14 | hibernateProps.put("hibernate.integrator_provider", 15 | (IntegratorProvider) () -> List.of(MetadataExtractorIntegrator.INSTANCE)); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/test/java/gt/app/config/MetadataExtractorIntegrator.java: -------------------------------------------------------------------------------- 1 | package gt.app.config; 2 | 3 | import lombok.Data; 4 | import org.hibernate.boot.Metadata; 5 | import org.hibernate.boot.model.relational.Database; 6 | import org.hibernate.engine.spi.SessionFactoryImplementor; 7 | import org.hibernate.integrator.spi.Integrator; 8 | import org.hibernate.service.spi.SessionFactoryServiceRegistry; 9 | 10 | @Data 11 | public class MetadataExtractorIntegrator implements Integrator { 12 | 13 | public static final MetadataExtractorIntegrator INSTANCE = 14 | new MetadataExtractorIntegrator(); 15 | private Database database; 16 | private Metadata metadata; 17 | 18 | @Override 19 | public void integrate(Metadata metadata, SessionFactoryImplementor sf, 20 | SessionFactoryServiceRegistry sr) { 21 | this.database = metadata.getDatabase(); 22 | this.metadata = metadata; 23 | } 24 | 25 | @Override 26 | public void disintegrate(SessionFactoryImplementor sf, 27 | SessionFactoryServiceRegistry sr) { 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/gt/app/e2e/WebAppIT.java: -------------------------------------------------------------------------------- 1 | package gt.app.e2e; 2 | 3 | import gt.app.config.Constants; 4 | import gt.app.e2e.pageobj.*; 5 | import gt.app.frwk.BaseSeleniumTest; 6 | import gt.app.frwk.TestDataManager; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.Test; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.test.context.ActiveProfiles; 11 | 12 | import static com.codeborne.selenide.Condition.text; 13 | 14 | @ActiveProfiles(Constants.SPRING_PROFILE_TEST) 15 | class WebAppIT extends BaseSeleniumTest { 16 | 17 | @Autowired 18 | TestDataManager testDataManager; 19 | 20 | @BeforeEach 21 | void cleanDB(){ 22 | testDataManager.truncateTablesAndRecreate(); 23 | } 24 | 25 | @Test 26 | void testPublicPage() { 27 | new PublicPage().open() 28 | .body() 29 | .shouldHave(text("Note App")) 30 | .shouldNotHave(text("Logout")) 31 | .shouldNotHave(text("Post Note")) 32 | 33 | .shouldHave(text("User2 Note")) 34 | .shouldHave(text("User1 Note")) 35 | .shouldHave(text("Admin's First Note")) 36 | .shouldHave(text("Admin's Second Note")) 37 | 38 | .shouldHave(text("Content Admin 1")) 39 | .shouldHave(text("Content Admin 2")) 40 | .shouldHave(text("Content User 1")) 41 | .shouldHave(text("Content User 2")); 42 | 43 | testAccessDenied(new PublicPage().open()); 44 | } 45 | 46 | void testAccessDenied(PublicPage publicPage) { 47 | 48 | publicPage.load("/note"); 49 | publicPage.body().shouldHave(text("Please sign in")); 50 | 51 | publicPage.load("/admin"); 52 | publicPage.body().shouldHave(text("Please sign in")); 53 | } 54 | 55 | @Test 56 | void testLoggedInUserPage() { 57 | 58 | var loginPage = new LoginPage().open(); 59 | 60 | LoggedInHomePage loggedInHomePage = loginPage.login("user1", "pass"); 61 | testLoggedInHomePage(loggedInHomePage); 62 | 63 | UserPage userPage = loggedInHomePage.openUserPage(); 64 | testUserPage(userPage); 65 | 66 | PublicPage publicPage = loggedInHomePage.logout(); 67 | publicPage.body() 68 | .shouldHave(text("You have been signed out")); 69 | 70 | testAccessDenied(publicPage); 71 | } 72 | 73 | private void testLoggedInHomePage(LoggedInHomePage page) { 74 | page.body() 75 | .shouldHave(text("Post Note")) 76 | .shouldHave(text("User1")); 77 | 78 | page.postNote("New Title", "New Content"); 79 | 80 | page.body() 81 | .shouldHave(text("New Title")) 82 | .shouldHave(text("New Content")); 83 | } 84 | 85 | private void testUserPage(UserPage page) { 86 | page.body() 87 | .shouldHave(text("Post Note")) 88 | .shouldHave(text("User1's Notes")) 89 | .shouldHave(text("Hello User1!")) 90 | .shouldHave(text("User1 Note")) 91 | //should not see other user's notes 92 | .shouldNotHave(text("Content Admin 1")) 93 | .shouldNotHave(text("Content Admin 2")) 94 | .shouldNotHave(text("User2's Note")) 95 | 96 | //previously created note 97 | .shouldHave(text("New Title")) 98 | .shouldHave(text("New Content")); 99 | 100 | //user gets redirected to home page after posting 101 | LoggedInHomePage homePage = page.postNote("Another Title", "Another Content"); 102 | 103 | homePage.body() 104 | .shouldHave(text("Another Title")) 105 | .shouldHave(text("Another Content")); 106 | 107 | //go back to user page again 108 | page = homePage.openUserPage(); 109 | 110 | //edit newly created note 111 | NoteEditPage editPage = page.editNote(1); 112 | editPage.body().shouldHave(text("Update Note")); 113 | 114 | homePage = editPage.updateNote("Updated Title", "Updated Content"); 115 | 116 | homePage.body() 117 | .shouldHave(text("Updated Title")) 118 | .shouldHave(text("Updated Content")) 119 | .shouldNotHave(text("Another Title")) 120 | .shouldNotHave(text("Another Content")); 121 | 122 | //go back to user page again 123 | page = homePage.openUserPage(); 124 | 125 | //delete 126 | PublicPage publicPage = page.deletePage(1); 127 | publicPage.body() 128 | .shouldHave(text("Note with id")) 129 | .shouldHave(text("is deleted")) 130 | .shouldNotHave(text("Updated Title")) 131 | .shouldNotHave(text("Updated Content")); 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /src/test/java/gt/app/e2e/pageobj/BaseLoggedInPage.java: -------------------------------------------------------------------------------- 1 | package gt.app.e2e.pageobj; 2 | 3 | import static com.codeborne.selenide.Selenide.$; 4 | 5 | public abstract class BaseLoggedInPage extends BasePage { 6 | 7 | public UserPage openUserPage() { 8 | $("#user-note-link").click(); 9 | return new UserPage(); 10 | } 11 | 12 | public PublicPage logout() { 13 | $("#navbarDropdownMenuLink").click(); 14 | $("#logout-link").click(); 15 | return new PublicPage(); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/test/java/gt/app/e2e/pageobj/BasePage.java: -------------------------------------------------------------------------------- 1 | package gt.app.e2e.pageobj; 2 | 3 | import com.codeborne.selenide.SelenideElement; 4 | 5 | import static com.codeborne.selenide.Selenide.$; 6 | 7 | public abstract class BasePage { 8 | 9 | public SelenideElement body() { 10 | return $("body"); 11 | } 12 | 13 | public abstract T open(); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/test/java/gt/app/e2e/pageobj/LoggedInHomePage.java: -------------------------------------------------------------------------------- 1 | package gt.app.e2e.pageobj; 2 | 3 | import com.codeborne.selenide.SelenideElement; 4 | 5 | import static com.codeborne.selenide.Selenide.$; 6 | 7 | public class LoggedInHomePage extends BaseLoggedInPage { 8 | 9 | @Override 10 | public LoggedInHomePage open() { 11 | return new LoggedInHomePage(); 12 | } 13 | 14 | 15 | public LoggedInHomePage postNote(String title, String content) { 16 | getTitle().setValue(title); 17 | getContent().setValue(content); 18 | 19 | getPostButton().pressEnter(); 20 | 21 | return this; 22 | } 23 | 24 | public SelenideElement getTitle() { 25 | return $("#title"); 26 | } 27 | 28 | public SelenideElement getContent() { 29 | return $("#content"); 30 | } 31 | 32 | public SelenideElement getPostButton() { 33 | return $("#postNote-btn"); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/gt/app/e2e/pageobj/LoginPage.java: -------------------------------------------------------------------------------- 1 | package gt.app.e2e.pageobj; 2 | 3 | import org.openqa.selenium.Keys; 4 | 5 | import static com.codeborne.selenide.Selenide.$; 6 | 7 | public class LoginPage extends BasePage { 8 | 9 | public LoggedInHomePage login(String username, String password) { 10 | 11 | $("#username").setValue(username); 12 | $("#password").setValue(password); 13 | $("#password").sendKeys(Keys.ENTER); 14 | 15 | return new LoggedInHomePage(); 16 | } 17 | 18 | 19 | @Override 20 | public LoginPage open() { 21 | var publicPage = new PublicPage(); 22 | publicPage.open(); 23 | return publicPage.clickLogin(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/gt/app/e2e/pageobj/NoteEditPage.java: -------------------------------------------------------------------------------- 1 | package gt.app.e2e.pageobj; 2 | 3 | import com.codeborne.selenide.SelenideElement; 4 | 5 | import static com.codeborne.selenide.Selenide.$; 6 | 7 | public class NoteEditPage extends BaseLoggedInPage { 8 | 9 | @Override 10 | public UserPage open() { 11 | return new UserPage(); 12 | } 13 | 14 | public LoggedInHomePage updateNote(String title, String content) { 15 | getTitle().setValue(title); 16 | getContent().setValue(content); 17 | 18 | getUpdateButton().pressEnter(); 19 | 20 | return new LoggedInHomePage(); 21 | } 22 | 23 | public SelenideElement getTitle() { 24 | return $("#title"); 25 | } 26 | 27 | public SelenideElement getContent() { 28 | return $("#content"); 29 | } 30 | 31 | public SelenideElement getUpdateButton() { 32 | return $("#updateNote-btn"); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/gt/app/e2e/pageobj/PublicPage.java: -------------------------------------------------------------------------------- 1 | package gt.app.e2e.pageobj; 2 | 3 | import com.codeborne.selenide.Selenide; 4 | 5 | import static com.codeborne.selenide.Selenide.$; 6 | 7 | public class PublicPage extends BasePage { 8 | 9 | public PublicPage open() { 10 | Selenide.open("/"); 11 | return this; 12 | } 13 | 14 | public LoginPage clickLogin() { 15 | open(); 16 | $("#login-link").click(); 17 | 18 | return new LoginPage(); 19 | } 20 | 21 | public void load(String url) { 22 | Selenide.open(url); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/gt/app/e2e/pageobj/UserPage.java: -------------------------------------------------------------------------------- 1 | package gt.app.e2e.pageobj; 2 | 3 | import com.codeborne.selenide.SelenideElement; 4 | 5 | import static com.codeborne.selenide.Selenide.$; 6 | import static com.codeborne.selenide.Selenide.$x; 7 | 8 | public class UserPage extends BaseLoggedInPage { 9 | 10 | @Override 11 | public UserPage open() { 12 | return new UserPage(); 13 | } 14 | 15 | public LoggedInHomePage postNote(String title, String content) { 16 | getTitle().setValue(title); 17 | getContent().setValue(content); 18 | 19 | getPostButton().pressEnter(); 20 | 21 | return new LoggedInHomePage(); 22 | } 23 | 24 | public SelenideElement getTitle() { 25 | return $("#title"); 26 | } 27 | 28 | public SelenideElement getContent() { 29 | return $("#content"); 30 | } 31 | 32 | public SelenideElement getPostButton() { 33 | return $("#postNote-btn"); 34 | } 35 | 36 | public NoteEditPage editNote(int row) { 37 | $x(".//table/tbody/tr[" + row + "]/td[5]/span/a").click(); 38 | return new NoteEditPage(); 39 | } 40 | 41 | public PublicPage deletePage(int row) { 42 | $x(".//table/tbody/tr[" + row + "]/td[6]/span/a").click(); 43 | return new PublicPage(); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/test/java/gt/app/frwk/BaseSeleniumTest.java: -------------------------------------------------------------------------------- 1 | package gt.app.frwk; 2 | 3 | import com.codeborne.selenide.Browsers; 4 | import com.codeborne.selenide.Configuration; 5 | import gt.app.config.Constants; 6 | import org.junit.jupiter.api.BeforeAll; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | import org.springframework.test.context.ActiveProfiles; 9 | 10 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, properties = "server.port=8081") 11 | @ActiveProfiles(Constants.SPRING_PROFILE_TEST) 12 | public abstract class BaseSeleniumTest { 13 | 14 | @BeforeAll 15 | public static void init() { 16 | Configuration.headless = true; 17 | Configuration.browser = Browsers.EDGE; 18 | 19 | 20 | Configuration.baseUrl = "http://localhost:8081"; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/test/java/gt/app/frwk/SampleTest.java: -------------------------------------------------------------------------------- 1 | package gt.app.frwk; 2 | 3 | import com.codeborne.selenide.Browsers; 4 | import com.codeborne.selenide.Configuration; 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import static com.codeborne.selenide.Condition.text; 9 | import static com.codeborne.selenide.Selenide.$; 10 | import static com.codeborne.selenide.Selenide.open; 11 | 12 | class SampleTest { 13 | 14 | @BeforeEach 15 | void setup() { 16 | Configuration.baseUrl = "https://en.wikipedia.org/wiki/Main_Page"; 17 | Configuration.headless = true; 18 | Configuration.browser = Browsers.EDGE; 19 | } 20 | 21 | @Test 22 | void test() { 23 | open("/"); 24 | 25 | $("[name=\"search\"]").setValue("Software"); 26 | 27 | $("[name=\"search\"]").pressEnter(); 28 | 29 | $("body").shouldHave(text("Software")); 30 | } 31 | 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/test/java/gt/app/frwk/TestDataManager.java: -------------------------------------------------------------------------------- 1 | package gt.app.frwk; 2 | 3 | import gt.app.DataCreator; 4 | import gt.app.config.MetadataExtractorIntegrator; 5 | import lombok.RequiredArgsConstructor; 6 | import org.hibernate.boot.Metadata; 7 | import org.hibernate.mapping.Collection; 8 | import org.hibernate.mapping.PersistentClass; 9 | import org.springframework.beans.factory.InitializingBean; 10 | import org.springframework.stereotype.Service; 11 | import org.springframework.transaction.annotation.Transactional; 12 | 13 | import jakarta.persistence.EntityManager; 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | 17 | @Service 18 | @RequiredArgsConstructor 19 | public class TestDataManager implements InitializingBean { 20 | final EntityManager em; 21 | final DataCreator dataCreator; 22 | 23 | private final List tableNames = new ArrayList<>(); //shared 24 | 25 | @Override 26 | public void afterPropertiesSet() { 27 | Metadata metadata = MetadataExtractorIntegrator.INSTANCE.getMetadata(); 28 | 29 | for (Collection persistentClass : metadata.getCollectionBindings()) { 30 | tableNames.add(persistentClass.getCollectionTable().getExportIdentifier()); 31 | } 32 | 33 | for (PersistentClass persistentClass : metadata.getEntityBindings()) { 34 | tableNames.add(persistentClass.getTable().getExportIdentifier()); 35 | } 36 | 37 | } 38 | 39 | @Transactional 40 | public void truncateTablesAndRecreate() { 41 | truncateTables(); 42 | dataCreator.initData(); 43 | } 44 | 45 | protected void truncateTables() { 46 | 47 | //for H2 48 | em.createNativeQuery("SET REFERENTIAL_INTEGRITY FALSE").executeUpdate(); 49 | for (String tableName : tableNames) { 50 | em.createNativeQuery("TRUNCATE TABLE " + tableName).executeUpdate(); 51 | } 52 | em.createNativeQuery("SET REFERENTIAL_INTEGRITY TRUE").executeUpdate(); 53 | 54 | // //for MySQL: 55 | // em.createNativeQuery("SET @@foreign_key_checks = 0").executeUpdate(); 56 | // for (String tableName : tableNames) { 57 | // em.createNativeQuery("TRUNCATE TABLE " + tableName).executeUpdate(); 58 | // } 59 | // em.createNativeQuery("SET @@foreign_key_checks = 1").executeUpdate(); 60 | 61 | } 62 | 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/test/java/gt/app/frwk/TestUtil.java: -------------------------------------------------------------------------------- 1 | package gt.app.frwk; 2 | 3 | import java.net.URL; 4 | 5 | import static java.lang.Thread.currentThread; 6 | 7 | public class TestUtil { 8 | 9 | public static URL fileFromClassPath(String name) { 10 | URL resource = currentThread().getContextClassLoader().getResource(name); 11 | if (resource == null) { 12 | throw new IllegalArgumentException("File " + name + " not found in classpath."); 13 | } 14 | return resource; 15 | } 16 | 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/test/java/gt/app/modules/file/FileDownloadUtilTest.java: -------------------------------------------------------------------------------- 1 | package gt.app.modules.file; 2 | 3 | import gt.app.frwk.TestUtil; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.mock.web.MockHttpServletResponse; 6 | import org.springframework.util.MimeTypeUtils; 7 | 8 | import java.io.IOException; 9 | import java.net.URL; 10 | 11 | import static org.assertj.core.api.Assertions.assertThat; 12 | 13 | class FileDownloadUtilTest { 14 | 15 | @Test 16 | void downloadFile() throws IOException { 17 | 18 | URL toDownload = TestUtil.fileFromClassPath("blob/test.txt"); 19 | 20 | MockHttpServletResponse resp = new MockHttpServletResponse(); 21 | 22 | FileDownloadUtil.downloadFile(resp, toDownload, "original.txt"); 23 | 24 | 25 | assertThat(resp.getHeader("Content-Disposition")).isEqualTo("attachment; filename=original.txt"); 26 | assertThat(resp.getContentType()).isEqualTo(MimeTypeUtils.APPLICATION_OCTET_STREAM.getType()); 27 | 28 | assertThat(resp.getContentAsString()).contains("Some Content"); 29 | assertThat(resp.getContentAsString()).contains("Content at the end"); 30 | 31 | } 32 | 33 | 34 | @Test 35 | void downloadFileWithContentType() throws IOException { 36 | 37 | URL toDownload = TestUtil.fileFromClassPath("blob/test.txt"); 38 | 39 | MockHttpServletResponse resp = new MockHttpServletResponse(); 40 | 41 | FileDownloadUtil.downloadFile(resp, toDownload, "original.txt", "mimetype"); 42 | 43 | 44 | assertThat(resp.getHeader("Content-Disposition")).isEqualTo("attachment; filename=original.txt"); 45 | assertThat(resp.getContentType()).isEqualTo("mimetype"); 46 | 47 | assertThat(resp.getContentAsString()).contains("Some Content"); 48 | assertThat(resp.getContentAsString()).contains("Content at the end"); 49 | 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/test/java/gt/app/web/rest/AppUserResourceIT.java: -------------------------------------------------------------------------------- 1 | package gt.app.web.rest; 2 | 3 | import gt.app.config.Constants; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | import org.springframework.security.test.context.support.WithMockUser; 9 | import org.springframework.test.context.ActiveProfiles; 10 | import org.springframework.test.web.servlet.MockMvc; 11 | 12 | import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin; 13 | import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated; 14 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 15 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; 16 | 17 | @SpringBootTest 18 | @AutoConfigureMockMvc 19 | @ActiveProfiles(Constants.SPRING_PROFILE_TEST) 20 | class AppUserResourceIT { 21 | 22 | @Test 23 | void getAccount3xx(@Autowired MockMvc mvc) throws Exception { 24 | mvc.perform(get("/api/account")) 25 | .andExpect(status().is3xxRedirection()); 26 | } 27 | 28 | @Test 29 | @WithMockUser 30 | void getAccount5xx(@Autowired MockMvc mvc) throws Exception { 31 | mvc.perform(get("/api/account")) 32 | .andExpect(status().isOk()) 33 | .andExpect(authenticated().withRoles("USER").withUsername("user")); 34 | } 35 | 36 | @Test 37 | @WithMockUser(value = "user1", authorities = "ROLE_USER") 38 | void getAccountJson(@Autowired MockMvc mvc) throws Exception { 39 | 40 | mvc.perform(get("/api/account")) 41 | .andExpect(status().isOk()) 42 | .andExpect(authenticated().withRoles("USER").withUsername("user1")) 43 | .andExpect(jsonPath("$.authorities").isArray()) 44 | .andExpect(jsonPath("$.authorities[0].authority").value("ROLE_USER")) 45 | .andExpect(jsonPath("$.username").value("user1")); 46 | } 47 | 48 | @Test 49 | void loginAndGetUser(@Autowired MockMvc mvc) throws Exception { 50 | 51 | mvc.perform(formLogin().loginProcessingUrl("/auth/login").user("system").password("pass")) 52 | .andExpect(authenticated().withUsername("system")) 53 | .andExpect(status().is3xxRedirection()) 54 | .andExpect(redirectedUrl("/")); 55 | } 56 | 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/gt/app/web/rest/HelloResourceIT.java: -------------------------------------------------------------------------------- 1 | package gt.app.web.rest; 2 | 3 | import gt.app.config.Constants; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | import org.springframework.http.MediaType; 9 | import org.springframework.security.test.context.support.WithMockUser; 10 | import org.springframework.test.context.ActiveProfiles; 11 | import org.springframework.test.web.servlet.MockMvc; 12 | 13 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 14 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; 15 | 16 | @SpringBootTest 17 | @AutoConfigureMockMvc 18 | @ActiveProfiles(Constants.SPRING_PROFILE_TEST) 19 | class HelloResourceIT { 20 | 21 | @Test 22 | @WithMockUser 23 | void sayHello2(@Autowired MockMvc mvc) throws Exception { 24 | 25 | mvc.perform(get("/debug/hello")) 26 | .andExpect(status().isOk()) 27 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) 28 | .andExpect(jsonPath("$.hello").value("world")); 29 | 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/resources/application-test.yml: -------------------------------------------------------------------------------- 1 | springdoc: 2 | api-docs: 3 | enabled: false 4 | spring: 5 | main: 6 | lazy-initialization: true 7 | banner-mode: off 8 | jpa: 9 | show-sql: false 10 | data: 11 | jpa: 12 | repositories: 13 | bootstrap-mode: lazy 14 | jmx: 15 | enabled: false 16 | datasource: 17 | url: jdbc:h2:mem:%s;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;DATABASE_TO_UPPER=false 18 | logging.level: 19 | ROOT: ERROR 20 | -------------------------------------------------------------------------------- /src/test/resources/archunit.properties: -------------------------------------------------------------------------------- 1 | archRule.failOnEmptyShould = false 2 | -------------------------------------------------------------------------------- /src/test/resources/blob/test.txt: -------------------------------------------------------------------------------- 1 | Some Content 2 | 3 | 4 | 5 | Content at the end 6 | 7 | -------------------------------------------------------------------------------- /src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | --------------------------------------------------------------------------------
Error java.lang.NullPointerException