├── .dockerignore ├── .gitignore ├── .travis.yml ├── Dockerfile ├── README.md ├── docker-compose.yml ├── images ├── screenshot1.png ├── screenshot10.png ├── screenshot11.png ├── screenshot2.png ├── screenshot3.png ├── screenshot4.png ├── screenshot5.png ├── screenshot6.png ├── screenshot7.png ├── screenshot8.png └── screenshot9.png ├── pom.xml └── src └── main ├── java └── com │ └── kaluzny │ └── demo │ ├── Application.java │ ├── config │ ├── JMSConfig.java │ ├── OpenApiConfig.java │ └── SecurityConfig.java │ ├── domain │ ├── Automobile.java │ └── AutomobileRepository.java │ ├── exception │ ├── AutoWasDeletedException.java │ ├── AwesomeExceptionHandler.java │ └── ThereIsNoSuchAutoException.java │ ├── listener │ └── Consumer.java │ └── web │ ├── AutomobileOpenApi.java │ ├── AutomobileResource.java │ ├── AutomobileRestController.java │ └── JMSPublisher.java └── resources ├── application.yml └── body.json /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .settings 3 | .idea 4 | **/*.class 5 | *.md 6 | 7 | **/target 8 | **/build 9 | **/bin 10 | *.text 11 | .gradle 12 | .settings 13 | **/*.project 14 | **/*.log 15 | **/*.bat -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/** 5 | !**/src/test/** 6 | 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | 17 | ### IntelliJ IDEA ### 18 | .idea 19 | *.iws 20 | *.iml 21 | *.ipr 22 | 23 | ### NetBeans ### 24 | /nbproject/private/ 25 | /nbbuild/ 26 | /dist/ 27 | /nbdist/ 28 | /.nb-gradle/ 29 | build/ 30 | 31 | ### VS Code ### 32 | .vscode/ 33 | 34 | .mvn 35 | mvnw.cmd 36 | mvnw -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - openjdk8 4 | dist: trusty 5 | sudo: 6 | required -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:17 2 | ADD /target/spring-boot-keycloak-docker-postgres.jar spring-boot-keycloak-docker-postgres.jar 3 | ENTRYPOINT ["java", "-jar", "spring-boot-keycloak-docker-postgres.jar"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Lightweight RESTful API with Spring Boot 3, Keycloak 21, Docker, PostgreSQL, JPA, Lombok, OpenAPI, etc. 3 | 4 | [![Build Status](https://travis-ci.org/OKaluzny/spring-boot-docker-postgres.svg?branch=master)](https://travis-ci.org/OKaluzny/spring-boot-docker-postgres) 5 | 6 | ## How it works: 7 | ### **1. Docker. First, you need to install docker** 8 | * Download Docker [Here](https://docs.docker.com/docker-for-windows/install/). Hint: Enable Hyper-V feature on windows and restart; 9 | * Then open powershell and check: 10 | ```bash 11 | docker info 12 | ``` 13 | or check docker version 14 | ```bash 15 | docker -v 16 | ``` 17 | or docker compose version 18 | ```bash 19 | docker-compose -v 20 | ``` 21 | ### **2. Spring boot app** 22 | * Clone the repository: 23 | ```bash 24 | git clone https://github.com/OKaluzny/spring-boot-docker-postgres.git 25 | ``` 26 | * Build the maven project: 27 | ```bash 28 | mvn clean install 29 | ``` 30 | * Running the containers: 31 | 32 | This command will build the docker containers and start them. 33 | ```bash 34 | docker-compose up 35 | ``` 36 | or 37 | 38 | This is a similar command as above, except it will run all the processes in the background. 39 | ```bash 40 | docker-compose -f docker-compose.yml up 41 | ``` 42 | 43 | Appendix A. 44 | 45 | All commands should be run from project root (where docker-compose.yml locates) 46 | 47 | * If you have to want to see running containers. Checklist docker containers 48 | ```bash 49 | docker container list -a 50 | ``` 51 | or 52 | ```bash 53 | docker-compose ps 54 | ``` 55 | 56 | ![Screenshot docker containers list](/images/screenshot1.png) 57 | *Screenshot with runnings containers* 58 | 59 | ### **3. Keycloak** 60 | 61 | Create Initial Admin User 62 | go to [http://localhost:8080/auth](http://localhost:8080/auth) and fill in the create initial admin form with username as `admin` and password as the `Pa55w0rd`. 63 | 64 | ![Screenshot](/images/screenshot2.png) 65 | *Keycloak — Create Initial Admin* 66 | 67 | Click Create and proceed to administration console and then login with your created initial admin. 68 | 69 | ![Screenshot](/images/screenshot3.png) 70 | *Keycloak — Log In Admin* 71 | 72 | The first screen Keycloak shows after login is the Master realm details. 73 | 74 | ![Screenshot](/images/screenshot4.png) 75 | *Keycloak — Master Realm Details* 76 | 77 | ### 3.1 Create a Realm 78 | 79 | Let’s name our first realm `automobile`: 80 | 81 | ![Screenshot](/images/screenshot5.png) 82 | *Keycloak — automobile* 83 | 84 | ### 3.2 Create a Role 85 | 86 | Roles are used to categorize the user. In an application, the permission to access resources is often granted to the role rather than the user. Admin, User, and Manager are all typical roles that may exist in an organization. 87 | 88 | To create a role, click the “Roles” menu on the left followed by the “Add Role” button on the page. 89 | 90 | ![Screenshot](/images/screenshot6.png) 91 | *Keycloak — Add Role* 92 | 93 | ### 3.3 Create a User 94 | 95 | Keycloak does not come with any pre-created user, so let’s create our first user, “Oleg” Click on the `Users` menu on the left and then click the `Add User` button. 96 | 97 | ![Screenshot](/images/screenshot7.png) 98 | *Keycloak — Add User* 99 | 100 | Oieg will require a password to login to Keycloak. Create a password credential for user your Oleg. Click on the “Credentials” tab and write both password and password confirmation as the `password`. Turn off the `Temporary password` switch so we do not need to change the password on the next login. Click “Set Password” to save your password credential. 101 | Finally, you need to assign the created `PERSON` role to Oleg. To do this, click on the Role Mappings tab, select the PERSON role, and click on “add selected”. 102 | 103 | ### 3.4 Add a Client 104 | 105 | Clients are entities that will request the authentication of a user. Clients come in two forms. The first type of client is an application that wants to participate in single-sign-on. These clients just want Keycloak to provide security for them. The other type of client is one that is requesting an access token so that it can invoke other services on behalf of the authenticated user. 106 | 107 | Let’s create a client that we will use to secure our Spring Boot REST service. 108 | 109 | Click on the Clients menu on the left and then click on Add Client. 110 | 111 | ![Screenshot](/images/screenshot8.png) 112 | *Keycloak — Add Client* 113 | 114 | In the form, fill Client Id as `app`, select `OpenID Connect` for the Client Protocol and click Save. 115 | In the client details form, we need to change the `Access Type` to `confidential` instead of defaulted public (where client secret is not required). Turn on the `“Authorization Enabled”` switch before you save the details. 116 | 117 | ### 3.5 Request an Access Token 118 | 119 | A client requests a security token by making a Token Exchange request to the token endpoint in Keycloak using the HTTP POST method. 120 | 121 | Go to [http://localhost:8080/auth/realms/automobile/protocol/openid-connect/token](http://localhost:8080/auth/realms/automobile/protocol/openid-connect/token) 122 | 123 | ![Screenshot](/images/screenshot9.png) 124 | *Postman — Token Exchange Request* 125 | 126 | It is expected to receive a JSON response with an `access token` and `refresh token` together with other accompanying details. 127 | The received `access token` can be used in every request to a Keycloak secured resource by simply placing it as a bearer token in the `Authorization` header: 128 | 129 | ![Screenshot](/images/screenshot10.png) 130 | *Postman — Token Exchange Request* 131 | 132 | or generate new `access token` and `refresh token` 133 | 134 | ![Screenshot](/images/screenshot11.png) 135 | *Postman — Token Exchange Request* 136 | 137 | ### **Guide for using endpoints the app:** 138 | 139 | Go to [http://localhost:8088/demo/api/automobiles](http://localhost:8088/demo/api/automobiles) to test and would specify OAuth 2.0 authorization redirect a username: `oleg` and password: `admin` 140 | 141 | * GET request to `/api/automobiles/` returns a list of "automobiles"; 142 | * GET request to `/api/automobiles/1` returns the "automobile" with ID 1; 143 | * POST request to `/api/automobiles/` with a "automobile" object as JSON creates a new "automobile"; 144 | * PUT request to `/api/automobiles/3` with a "automobile" object as JSON updates the "automobile" with ID 3; 145 | * DELETE request to `/api/automobiles/4` deletes the "automobile" with ID 4; 146 | * DELETE request to `/api/automobiles/` deletes all the "automobiles". 147 | --- 148 | * GET request to `/api/automobiles?color=madeira-violet` returns the "automobile"`s with color madeira-violet; 149 | * GET request to `/api/automobiles?name=BMW&color=techno-violet` returns the "automobile"`s with name BMW and color techno-violet; 150 | * GET request to `/api/automobiles?colorStartsWith=Ma&page=0&size=2` returns the "automobile"`s with color which starts with "m". Included Pagination and sorting; 151 | 152 | or use Swagger API [http://localhost:8088/demo/swagger-ui.html](http://localhost:8088/demo/swagger-ui.html) 153 | 154 | and generation API docks [http://localhost:8088/demo/v3/api-docs.yaml](http://localhost:8088/demo/v3/api-docs.yaml) 155 | 156 | Appendix B. 157 | 158 | * Do not forget, if you see db, open the Windows Services Manager on your Windows 10 computer and stop postgres 159 | 160 | ### **4. Docker control commands** 161 | * Check all the images you have: 162 | ```bash 163 | docker images 164 | ``` 165 | ### **5. End stop app** 166 | * Stop containers: 167 | ```bash 168 | docker-compose down 169 | ``` 170 | * Remove old stopped containers of docker-compose 171 | ```bash 172 | docker-compose rm -f 173 | ``` 174 | 175 | 176 | 177 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Docker Compose file Reference (https://docs.docker.com/compose/compose-file/) 2 | version: '3.9' 3 | # Define services 4 | services: 5 | # App backend service 6 | #app: 7 | # This service depends on postgres db and keycloak auth. Start that first. 8 | # depends_on: 9 | # db: 10 | # condition: service_healthy 11 | #keycloak: 12 | # condition: service_started 13 | #image: spring-boot-keycloak-docker-postgres:latest 14 | #build: 15 | # context: ./ 16 | # dockerfile: "Dockerfile" 17 | # Give the container the name web-app. You can change to something else. 18 | #container_name: web-app 19 | # Forward the exposed port 8080 on the container to port 8080 on the host machine 20 | #ports: 21 | # - "0.0.0.0:8088:8080/tcp" 22 | # - target: 8080 23 | # host_ip: 0.0.0.0 24 | # published: 8088 25 | #protocol: tcp 26 | #mode: host 27 | #ports: 28 | # - "8080:8080" 29 | #networks: 30 | # - backend 31 | # entrypoint: [ "java", "-Xms512m", "-Xmx1g", "-jar" ] 32 | # Database Service (Postgres) 33 | #db: 34 | # Give the container the name postgres-db. You can change to something else. 35 | # container_name: postgres-db 36 | # Use the Docker Image postgres. This will pull the 14 version. 37 | #image: postgres:14-alpine 38 | #healthcheck: 39 | # test: [ "CMD", "pg_isready", "-q", "-d", "postgres", "-U" ] 40 | #timeout: 45s 41 | #interval: 10s 42 | #retries: 10 43 | #restart: always 44 | # Set a volume some that database is not lost after shutting down the container. 45 | # I used the name postgres-data, but you can change it to something else. 46 | #volumes: 47 | # - postgres_data_keycloak:/var/lib/postgresql/data 48 | #networks: 49 | # - backend 50 | #network_mode: host 51 | # Maps port 5432 (localhost) to port 5432 on the container. You can change the ports to fix your needs. 52 | #ports: 53 | # - "5432:5432" 54 | # Set up the username, password, and database name. You can change these values. 55 | #environment: 56 | # POSTGRES_USER: postgres 57 | #POSTGRES_PASSWORD: postgres 58 | #POSTGRES_DB: automobiles 59 | #PGDATA: /var/lib/postgresql/data/pgdata 60 | # Auth service 61 | keycloak: 62 | container_name: keycloak-auth 63 | image: quay.io/keycloak/keycloak:22.0.1 64 | #build: 65 | # context: . 66 | #args: 67 | # KEYCLOAK_VERSION: 22.0.1 68 | command: 69 | - "start-dev" 70 | ports: 71 | - "8180:8080" 72 | networks: 73 | - keycloak 74 | environment: 75 | KEYCLOAK_ADMIN: admin 76 | KEYCLOAK_ADMIN_PASSWORD: password 77 | KC_DB: postgres 78 | KC_DB_URL_HOST: keycloak-db 79 | KC_DB_URL_DATABASE: keycloak 80 | KC_DB_USERNAME: keycloak 81 | KC_DB_PASSWORD: password 82 | KC_HEALTH_ENABLED: true 83 | depends_on: 84 | - keycloak-db 85 | #volumes: 86 | # - /home/keycloak/automobile-realm.json:/opt/keycloak/data/import/automobile-realm.json 87 | # Database Service (Postgres) for Keycloak 88 | keycloak-db: 89 | image: postgres:14-alpine 90 | container_name: keycloak-db 91 | ports: 92 | - "5433:5432" 93 | volumes: 94 | - postgres_data_keycloak:/var/lib/postgresql/data 95 | environment: 96 | POSTGRES_DB: keycloak 97 | POSTGRES_USER: keycloak 98 | POSTGRES_PASSWORD: password 99 | networks: [ keycloak ] 100 | healthcheck: 101 | test: [ "CMD", "pg_isready", "-q", "-d", "postgres", "-U" ] 102 | timeout: 45s 103 | interval: 10s 104 | retries: 10 105 | 106 | activemq: 107 | image: webcenter/activemq:latest 108 | ports: 109 | # mqtt 110 | - "1883:1883" 111 | # amqp 112 | - "5672:5672" 113 | # ui 114 | - "8161:8161" 115 | # stomp 116 | - "61613:61613" 117 | # ws 118 | - "61614:61614" 119 | # jms 120 | - "61616:61616" 121 | networks: [ activemq ] 122 | volumes: [ "activemq-data:/opt/activemq/conf", "activemq-data:/data/activemq", "activemq-data:/var/log/activemq" ] 123 | environment: 124 | ACTIVEMQ_REMOVE_DEFAULT_ACCOUNT: "true" 125 | ACTIVEMQ_ADMIN_LOGIN: admin 126 | ACTIVEMQ_ADMIN_PASSWORD: password 127 | ACTIVEMQ_WRITE_LOGIN: write 128 | ACTIVEMQ_WRITE_PASSWORD: password 129 | ACTIVEMQ_READ_LOGIN: read 130 | ACTIVEMQ_READ_PASSWORD: password 131 | ACTIVEMQ_JMX_LOGIN: jmx 132 | ACTIVEMQ_JMX_PASSWORD: password 133 | 134 | ACTIVEMQ_STATIC_TOPICS: static-topic-1;static-topic-2;autoTopic 135 | ACTIVEMQ_STATIC_QUEUES: static-queue-1;static-queue-2 136 | ACTIVEMQ_ENABLED_SCHEDULER: "true" 137 | ACTIVEMQ_MIN_MEMORY: 512 138 | ACTIVEMQ_MAX_MEMORY: 2048 139 | 140 | networks: 141 | # backend: 142 | # name: app 143 | # driver: bridge 144 | keycloak: 145 | name: keycloak 146 | driver: bridge 147 | activemq: { } 148 | 149 | volumes: 150 | postgres_data_keycloak: 151 | driver: local 152 | activemq-data: 153 | driver: local 154 | -------------------------------------------------------------------------------- /images/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OKaluzny/spring-boot-docker-postgres/e2db14d96e18f00931b27a09a28307a8697980c9/images/screenshot1.png -------------------------------------------------------------------------------- /images/screenshot10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OKaluzny/spring-boot-docker-postgres/e2db14d96e18f00931b27a09a28307a8697980c9/images/screenshot10.png -------------------------------------------------------------------------------- /images/screenshot11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OKaluzny/spring-boot-docker-postgres/e2db14d96e18f00931b27a09a28307a8697980c9/images/screenshot11.png -------------------------------------------------------------------------------- /images/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OKaluzny/spring-boot-docker-postgres/e2db14d96e18f00931b27a09a28307a8697980c9/images/screenshot2.png -------------------------------------------------------------------------------- /images/screenshot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OKaluzny/spring-boot-docker-postgres/e2db14d96e18f00931b27a09a28307a8697980c9/images/screenshot3.png -------------------------------------------------------------------------------- /images/screenshot4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OKaluzny/spring-boot-docker-postgres/e2db14d96e18f00931b27a09a28307a8697980c9/images/screenshot4.png -------------------------------------------------------------------------------- /images/screenshot5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OKaluzny/spring-boot-docker-postgres/e2db14d96e18f00931b27a09a28307a8697980c9/images/screenshot5.png -------------------------------------------------------------------------------- /images/screenshot6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OKaluzny/spring-boot-docker-postgres/e2db14d96e18f00931b27a09a28307a8697980c9/images/screenshot6.png -------------------------------------------------------------------------------- /images/screenshot7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OKaluzny/spring-boot-docker-postgres/e2db14d96e18f00931b27a09a28307a8697980c9/images/screenshot7.png -------------------------------------------------------------------------------- /images/screenshot8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OKaluzny/spring-boot-docker-postgres/e2db14d96e18f00931b27a09a28307a8697980c9/images/screenshot8.png -------------------------------------------------------------------------------- /images/screenshot9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OKaluzny/spring-boot-docker-postgres/e2db14d96e18f00931b27a09a28307a8697980c9/images/screenshot9.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | org.springframework.boot 8 | spring-boot-starter-parent 9 | 3.1.2 10 | 11 | 12 | 13 | com.kaluzny 14 | spring-boot-keycloak-docker-postgres 15 | 0.0.1-SNAPSHOT 16 | spring-boot-keycloak-docker-postgres 17 | Demo project for Spring Boot, Keycloak, Postgres, Docker and Spotify plugin 18 | 19 | 20 | 17 21 | 2.1.0 22 | 21.0.2 23 | 1.18.28 24 | 25 | 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-data-jpa 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-web 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-starter-validation 38 | 39 | 40 | org.springframework.boot 41 | spring-boot-starter-actuator 42 | 43 | 44 | org.springframework.boot 45 | spring-boot-starter-oauth2-client 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-starter-oauth2-resource-server 50 | 51 | 52 | 53 | org.springframework.boot 54 | spring-boot-starter-security 55 | 56 | 57 | 58 | org.postgresql 59 | postgresql 60 | runtime 61 | 62 | 63 | org.springframework.security 64 | spring-security-test 65 | test 66 | 67 | 68 | org.keycloak 69 | keycloak-spring-boot-starter 70 | ${keycloak.version} 71 | 72 | 73 | 74 | org.springdoc 75 | springdoc-openapi-starter-webmvc-ui 76 | ${openApi.version} 77 | 78 | 79 | 80 | org.projectlombok 81 | lombok 82 | ${org.project-lombok.version} 83 | provided 84 | 85 | 86 | 87 | org.springframework.boot 88 | spring-boot-starter-activemq 89 | 90 | 91 | org.apache.activemq 92 | activemq-broker 93 | 94 | 95 | com.fasterxml.jackson.core 96 | jackson-databind 97 | 98 | 99 | 100 | com.fasterxml.jackson.datatype 101 | jackson-datatype-jsr310 102 | 2.15.2 103 | 104 | 105 | 106 | 107 | 108 | spring-boot-keycloak-docker-postgres 109 | 110 | 111 | org.springframework.boot 112 | spring-boot-maven-plugin 113 | 114 | 115 | 116 | org.apache.maven.plugins 117 | maven-compiler-plugin 118 | 3.11.0 119 | 120 | ${java.version} 121 | ${java.version} 122 | 123 | 124 | org.projectlombok 125 | lombok 126 | ${org.project-lombok.version} 127 | 128 | 129 | 130 | 131 | -Amapstruct.defaultComponentModel=spring 132 | 133 | 134 | 135 | 136 | 160 | 161 | 162 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /src/main/java/com/kaluzny/demo/Application.java: -------------------------------------------------------------------------------- 1 | package com.kaluzny.demo; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cache.annotation.EnableCaching; 6 | 7 | @SpringBootApplication 8 | @EnableCaching 9 | public class Application { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(Application.class, args); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/kaluzny/demo/config/JMSConfig.java: -------------------------------------------------------------------------------- 1 | package com.kaluzny.demo.config; 2 | 3 | import org.apache.activemq.ActiveMQConnectionFactory; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.jms.annotation.EnableJms; 8 | import org.springframework.jms.config.DefaultJmsListenerContainerFactory; 9 | import org.springframework.jms.connection.CachingConnectionFactory; 10 | import org.springframework.jms.support.converter.MappingJackson2MessageConverter; 11 | import org.springframework.jms.support.converter.MessageConverter; 12 | import org.springframework.jms.support.converter.MessageType; 13 | 14 | @Configuration 15 | @EnableJms 16 | public class JMSConfig { 17 | 18 | @Value("${spring.activemq.broker-url}") 19 | private String brokerUrl; 20 | 21 | @Bean 22 | public DefaultJmsListenerContainerFactory automobileJmsContFactory() { 23 | DefaultJmsListenerContainerFactory containerFactory = new DefaultJmsListenerContainerFactory(); 24 | containerFactory.setPubSubDomain(true); 25 | containerFactory.setConnectionFactory(connectionFactory()); 26 | containerFactory.setMessageConverter(jacksonJmsMsgConverter()); 27 | containerFactory.setSubscriptionDurable(true); 28 | return containerFactory; 29 | } 30 | 31 | @Bean 32 | public CachingConnectionFactory connectionFactory() { 33 | ActiveMQConnectionFactory activeMQConnFactory = new ActiveMQConnectionFactory(); 34 | activeMQConnFactory.setBrokerURL(brokerUrl); 35 | CachingConnectionFactory factory = new CachingConnectionFactory(); 36 | factory.setTargetConnectionFactory(activeMQConnFactory); 37 | factory.setClientId("client123"); 38 | return factory; 39 | } 40 | 41 | @Bean 42 | public MessageConverter jacksonJmsMsgConverter() { 43 | MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter(); 44 | converter.setTargetType(MessageType.TEXT); 45 | converter.setTypeIdPropertyName("_type"); 46 | return converter; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/kaluzny/demo/config/OpenApiConfig.java: -------------------------------------------------------------------------------- 1 | package com.kaluzny.demo.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | 6 | import io.swagger.v3.oas.models.Components; 7 | import io.swagger.v3.oas.models.OpenAPI; 8 | import io.swagger.v3.oas.models.info.Info; 9 | 10 | /** 11 | * Swagger config. 12 | * 13 | * @author Oleg Kaluzny 14 | */ 15 | @Configuration 16 | public class OpenApiConfig { 17 | 18 | @Bean 19 | public OpenAPI customOpenAPI() { 20 | return new OpenAPI() 21 | .components(new Components()) 22 | .info(new Info() 23 | .title("Automobile API") 24 | .description(" Spring Boot RESTful service using springdoc-openapi and OpenAPI 3.")); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/kaluzny/demo/config/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.kaluzny.demo.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.http.HttpMethod; 6 | import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; 7 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 8 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 9 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 10 | import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; 11 | import org.springframework.security.web.SecurityFilterChain; 12 | 13 | import java.util.Collection; 14 | import java.util.Map; 15 | import java.util.stream.Collectors; 16 | 17 | @Configuration 18 | @EnableWebSecurity 19 | @EnableMethodSecurity 20 | class SecurityConfig { 21 | 22 | @Bean 23 | public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { 24 | 25 | httpSecurity 26 | //TODO: security without @PreAuthorize 27 | /* .authorizeHttpRequests(registry -> registry 28 | .requestMatchers(HttpMethod.GET, "/api/**").hasRole("USER") 29 | .requestMatchers(HttpMethod.POST, "/api/**").hasRole("PERSON") 30 | .anyRequest().authenticated() 31 | )*/ 32 | .oauth2ResourceServer(oauth2Configurer -> oauth2Configurer 33 | .jwt(jwtConfigurer -> jwtConfigurer 34 | .jwtAuthenticationConverter(jwt -> { 35 | Map> realmAccess = jwt.getClaim("realm_access"); 36 | Collection roles = realmAccess.get("roles"); 37 | 38 | var grantedAuthorities = roles.stream() 39 | .map(role -> new SimpleGrantedAuthority("ROLE_" + role)) 40 | .collect(Collectors.toList()); 41 | 42 | return new JwtAuthenticationToken(jwt, grantedAuthorities); 43 | }))); 44 | return httpSecurity.build(); 45 | } 46 | } -------------------------------------------------------------------------------- /src/main/java/com/kaluzny/demo/domain/Automobile.java: -------------------------------------------------------------------------------- 1 | package com.kaluzny.demo.domain; 2 | 3 | import com.fasterxml.jackson.annotation.JsonFormat; 4 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 5 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 6 | import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; 7 | import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; 8 | import io.swagger.v3.oas.annotations.media.Schema; 9 | import jakarta.persistence.*; 10 | import jakarta.validation.constraints.Size; 11 | import lombok.*; 12 | 13 | import java.time.LocalDateTime; 14 | 15 | 16 | @Entity 17 | @Getter 18 | @Setter 19 | @ToString 20 | @AllArgsConstructor 21 | @NoArgsConstructor 22 | @Builder 23 | @Schema(name = "Automobile", description = "Data object for an automobile", oneOf = Automobile.class) 24 | public class Automobile { 25 | 26 | @Schema(description = "Unique identifier of the Automobile.", example = "1") 27 | @Id 28 | @GeneratedValue(strategy = GenerationType.SEQUENCE) 29 | private Long id; 30 | 31 | @Schema(description = "Name of the Automobile.", example = "Volvo", required = true) 32 | @Size(max = 50) 33 | private String name; 34 | 35 | @Schema(description = "Color of the Automobile.", example = "Red", required = true) 36 | @Size(max = 50) 37 | private String color; 38 | 39 | @JsonDeserialize(using = LocalDateTimeDeserializer.class) 40 | @JsonSerialize(using = LocalDateTimeSerializer.class) 41 | @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy HH:mm:ss") 42 | private LocalDateTime creationDate = LocalDateTime.now(); 43 | 44 | @JsonDeserialize(using = LocalDateTimeDeserializer.class) 45 | @JsonSerialize(using = LocalDateTimeSerializer.class) 46 | @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy HH:mm:ss") 47 | private LocalDateTime updateDate = LocalDateTime.now(); 48 | 49 | @Column(name = "original_color") 50 | private Boolean originalColor = Boolean.TRUE; 51 | 52 | private Boolean deleted = Boolean.FALSE; 53 | 54 | public void checkColor(Automobile automobile) { 55 | if (automobile.color != null && !automobile.color.equals(this.color)) { 56 | this.originalColor = false; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/kaluzny/demo/domain/AutomobileRepository.java: -------------------------------------------------------------------------------- 1 | package com.kaluzny.demo.domain; 2 | 3 | import org.springframework.data.domain.Pageable; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | import java.util.List; 8 | 9 | @Repository 10 | public interface AutomobileRepository extends JpaRepository { 11 | List findByName(String name); 12 | 13 | List findByColor(String color); 14 | 15 | List findByNameAndColor(String name, String color); 16 | 17 | List findByColorStartsWith(String colorStartWith, Pageable page); 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/kaluzny/demo/exception/AutoWasDeletedException.java: -------------------------------------------------------------------------------- 1 | package com.kaluzny.demo.exception; 2 | 3 | public class AutoWasDeletedException extends RuntimeException { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/java/com/kaluzny/demo/exception/AwesomeExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.kaluzny.demo.exception; 2 | 3 | import org.springframework.http.HttpStatusCode; 4 | import org.springframework.http.ProblemDetail; 5 | import org.springframework.web.bind.annotation.ExceptionHandler; 6 | import org.springframework.web.bind.annotation.RestControllerAdvice; 7 | import org.springframework.web.client.HttpClientErrorException; 8 | import org.springframework.web.client.HttpServerErrorException; 9 | import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; 10 | 11 | import java.util.concurrent.TimeoutException; 12 | 13 | @RestControllerAdvice 14 | public class AwesomeExceptionHandler extends ResponseEntityExceptionHandler { 15 | 16 | @ExceptionHandler(RuntimeException.class) 17 | public ProblemDetail handleRuntimeException(RuntimeException ex) { 18 | return ProblemDetail.forStatusAndDetail(HttpStatusCode.valueOf(404), ex.getMessage()); 19 | } 20 | 21 | @ExceptionHandler(HttpClientErrorException.BadRequest.class) 22 | public ProblemDetail handleBadRequest(HttpClientErrorException.BadRequest ex) { 23 | return ProblemDetail.forStatusAndDetail(HttpStatusCode.valueOf(400), ex.getMessage()); 24 | } 25 | 26 | @ExceptionHandler(AutoWasDeletedException.class) 27 | public ProblemDetail handleDeleteException(AutoWasDeletedException ex) { 28 | ProblemDetail pd = ProblemDetail.forStatusAndDetail(HttpStatusCode.valueOf(404), ex.getMessage()); 29 | pd.setTitle("This auto was deleted"); 30 | return pd; 31 | } 32 | 33 | @ExceptionHandler(ThereIsNoSuchAutoException.class) 34 | public ProblemDetail handleThereIsNoSuchUserException(ThereIsNoSuchAutoException ex) { 35 | ProblemDetail pd = ProblemDetail.forStatusAndDetail(HttpStatusCode.valueOf(404), ex.getMessage()); 36 | pd.setTitle("There is no such automobile"); 37 | return pd; 38 | } 39 | 40 | @ExceptionHandler(HttpServerErrorException.InternalServerError.class) 41 | public ProblemDetail handleConnectException(HttpServerErrorException.InternalServerError ex) { 42 | return ProblemDetail.forStatusAndDetail(HttpStatusCode.valueOf(500), ex.getMessage()); 43 | } 44 | 45 | @ExceptionHandler(TimeoutException.class) 46 | public ProblemDetail handleTimeoutException(TimeoutException ex) { 47 | ProblemDetail pd = ProblemDetail.forStatusAndDetail(HttpStatusCode.valueOf(503), ex.getMessage()); 48 | pd.setTitle("Service Unavailable or DB connection was refused"); 49 | return pd; 50 | } 51 | } -------------------------------------------------------------------------------- /src/main/java/com/kaluzny/demo/exception/ThereIsNoSuchAutoException.java: -------------------------------------------------------------------------------- 1 | package com.kaluzny.demo.exception; 2 | 3 | public class ThereIsNoSuchAutoException extends RuntimeException { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/java/com/kaluzny/demo/listener/Consumer.java: -------------------------------------------------------------------------------- 1 | package com.kaluzny.demo.listener; 2 | 3 | import com.kaluzny.demo.domain.Automobile; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.jms.annotation.JmsListener; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Slf4j 9 | @Component 10 | public class Consumer { 11 | 12 | @JmsListener(destination = "AutoTopic", containerFactory = "automobileJmsContFactory") 13 | public void getAutomobileListener1(Automobile automobile) { 14 | log.info("\u001B[32m" + "Automobile Consumer 1: " + automobile + "\u001B[0m"); 15 | } 16 | 17 | @JmsListener(destination = "AutoTopic", containerFactory = "automobileJmsContFactory") 18 | public void getAutomobileListener2(Automobile automobile) { 19 | log.info("\u001B[33m" + "Automobile Consumer 2: " + automobile + "\u001B[0m"); 20 | } 21 | 22 | @JmsListener(destination = "AutoTopic", containerFactory = "automobileJmsContFactory") 23 | public void getAutomobileListener3(Automobile automobile) { 24 | log.info("\u001B[34m" + "Automobile Consumer 3: " + automobile + "\u001B[0m"); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/kaluzny/demo/web/AutomobileOpenApi.java: -------------------------------------------------------------------------------- 1 | package com.kaluzny.demo.web; 2 | 3 | import com.kaluzny.demo.domain.Automobile; 4 | import io.swagger.v3.oas.annotations.Operation; 5 | import io.swagger.v3.oas.annotations.Parameter; 6 | import io.swagger.v3.oas.annotations.media.ArraySchema; 7 | import io.swagger.v3.oas.annotations.media.Content; 8 | import io.swagger.v3.oas.annotations.media.Schema; 9 | import io.swagger.v3.oas.annotations.responses.ApiResponse; 10 | import io.swagger.v3.oas.annotations.responses.ApiResponses; 11 | import io.swagger.v3.oas.annotations.tags.Tag; 12 | import org.springframework.web.bind.annotation.PathVariable; 13 | 14 | import java.util.Collection; 15 | 16 | @Tag(name = "Automobile", description = "the Automobile API") 17 | public interface AutomobileOpenApi extends AutomobileResource { 18 | 19 | @Operation(summary = "Add a new Automobile", description = "endpoint for creating an entity", tags = {"Automobile"}) 20 | @ApiResponses(value = { 21 | @ApiResponse(responseCode = "201", description = "Automobile created"), 22 | @ApiResponse(responseCode = "400", description = "Invalid input"), 23 | @ApiResponse(responseCode = "409", description = "Automobile already exists")}) 24 | Automobile saveAutomobile(@Parameter(description = "Automobile", required = true) Automobile automobile); 25 | 26 | @Operation(summary = "Find all Automobiles", description = " ", tags = {"Automobile"}) 27 | @ApiResponses(value = { 28 | @ApiResponse(responseCode = "200", description = "successful operation", 29 | content = @Content(array = @ArraySchema(schema = @Schema(implementation = Automobile.class))))}) 30 | Collection getAllAutomobiles(); 31 | 32 | @Operation(summary = "Find automobile by ID", description = "Returns a single automobile", tags = {"Automobile"}) 33 | @ApiResponses(value = { 34 | @ApiResponse(responseCode = "200", description = "successful operation", 35 | content = @Content(schema = @Schema(implementation = Automobile.class))), 36 | @ApiResponse(responseCode = "404", description = "There is no such automobile")}) 37 | Automobile getAutomobileById( 38 | @Parameter(description = "Id of the Automobile to be obtained. Cannot be empty.", required = true) 39 | @PathVariable Long id); 40 | 41 | @Operation(summary = "Find automobile by name", description = " ", tags = {"Automobile"}) 42 | @ApiResponses(value = { 43 | @ApiResponse(responseCode = "200", description = "successful operation", 44 | content = @Content(array = @ArraySchema(schema = @Schema(implementation = Automobile.class))))}) 45 | Collection findAutomobileByName( 46 | @Parameter(description = "Name of the Automobile to be obtained. Cannot be empty.", required = true) String name); 47 | 48 | @Operation(summary = "Update an existing Automobile", description = "need to fill", tags = {"Automobile"}) 49 | @ApiResponses(value = { 50 | @ApiResponse(responseCode = "200", description = "successful operation"), 51 | @ApiResponse(responseCode = "400", description = "Invalid ID supplied"), 52 | @ApiResponse(responseCode = "404", description = "Automobile not found"), 53 | @ApiResponse(responseCode = "405", description = "Validation exception")}) 54 | Automobile refreshAutomobile( 55 | @Parameter(description = "Id of the Automobile to be update. Cannot be empty.", required = true) Long id, 56 | @Parameter(description = "Automobile to update.", required = true) Automobile automobile); 57 | 58 | @Operation(summary = "Deletes a Automobile", description = "need to fill", tags = {"Automobile"}) 59 | @ApiResponses(value = { 60 | @ApiResponse(responseCode = "200", description = "successful operation"), 61 | @ApiResponse(responseCode = "404", description = "Automobile not found")}) 62 | String removeAutomobileById( 63 | @Parameter(description = "Id of the Automobile to be delete. Cannot be empty.", required = true) Long id); 64 | 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/kaluzny/demo/web/AutomobileResource.java: -------------------------------------------------------------------------------- 1 | package com.kaluzny.demo.web; 2 | 3 | import com.kaluzny.demo.domain.Automobile; 4 | 5 | import java.util.Collection; 6 | import java.util.List; 7 | 8 | public interface AutomobileResource { 9 | 10 | Automobile saveAutomobile(Automobile automobile); 11 | 12 | Collection getAllAutomobiles(); 13 | 14 | Automobile getAutomobileById(Long id); 15 | 16 | Collection findAutomobileByName(String name); 17 | 18 | Automobile refreshAutomobile(Long id, Automobile automobile); 19 | 20 | String removeAutomobileById(Long id); 21 | 22 | void removeAllAutomobiles(); 23 | 24 | Collection findAutomobileByColor(String color); 25 | 26 | Collection findAutomobileByNameAndColor(String name, String color); 27 | 28 | Collection findAutomobileByColorStartsWith(String colorStartsWith, int page, int size); 29 | 30 | List getAllAutomobilesByName(); 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/kaluzny/demo/web/AutomobileRestController.java: -------------------------------------------------------------------------------- 1 | package com.kaluzny.demo.web; 2 | 3 | import com.kaluzny.demo.domain.Automobile; 4 | import com.kaluzny.demo.domain.AutomobileRepository; 5 | import com.kaluzny.demo.exception.AutoWasDeletedException; 6 | import com.kaluzny.demo.exception.ThereIsNoSuchAutoException; 7 | import io.swagger.v3.oas.annotations.Hidden; 8 | import io.swagger.v3.oas.annotations.Parameter; 9 | import jakarta.annotation.PostConstruct; 10 | import jakarta.jms.Topic; 11 | import jakarta.validation.Valid; 12 | import lombok.RequiredArgsConstructor; 13 | import lombok.extern.slf4j.Slf4j; 14 | import org.springframework.cache.annotation.CacheEvict; 15 | import org.springframework.data.domain.PageRequest; 16 | import org.springframework.data.domain.Sort; 17 | import org.springframework.http.HttpStatus; 18 | import org.springframework.http.MediaType; 19 | import org.springframework.http.ResponseEntity; 20 | import org.springframework.jms.core.JmsTemplate; 21 | import org.springframework.security.access.prepost.PreAuthorize; 22 | import org.springframework.transaction.annotation.Transactional; 23 | import org.springframework.web.bind.annotation.*; 24 | 25 | import java.time.Duration; 26 | import java.time.Instant; 27 | import java.time.LocalDateTime; 28 | import java.util.Collection; 29 | import java.util.List; 30 | import java.util.Objects; 31 | import java.util.stream.Collectors; 32 | 33 | @RestController 34 | @RequestMapping(value = "/api", produces = MediaType.APPLICATION_JSON_VALUE) 35 | @RequiredArgsConstructor 36 | @Slf4j 37 | public class AutomobileRestController implements AutomobileResource, AutomobileOpenApi, JMSPublisher { 38 | 39 | private final AutomobileRepository repository; 40 | private final JmsTemplate jmsTemplate; 41 | 42 | public static double getTiming(Instant start, Instant end) { 43 | return Duration.between(start, end).toMillis(); 44 | } 45 | 46 | @Transactional 47 | @PostConstruct 48 | public void init() { 49 | repository.save(new Automobile(1L, "Ford", "Green", LocalDateTime.now(), LocalDateTime.now(), true, false)); 50 | } 51 | 52 | @PostMapping("/automobiles") 53 | @ResponseStatus(HttpStatus.CREATED) 54 | @PreAuthorize("hasRole('ADMIN')") 55 | //@RolesAllowed("ADMIN") 56 | public Automobile saveAutomobile(@Valid @RequestBody Automobile automobile) { 57 | log.info("saveAutomobile() - start: automobile = {}", automobile); 58 | Automobile savedAutomobile = repository.save(automobile); 59 | log.info("saveAutomobile() - end: savedAutomobile = {}", savedAutomobile.getId()); 60 | return savedAutomobile; 61 | } 62 | 63 | @GetMapping("/automobiles") 64 | @ResponseStatus(HttpStatus.OK) 65 | //@Cacheable(value = "automobile", sync = true) 66 | @PreAuthorize("hasRole('USER')") 67 | public Collection getAllAutomobiles() { 68 | log.info("getAllAutomobiles() - start"); 69 | Collection collection = repository.findAll(); 70 | log.info("getAllAutomobiles() - end"); 71 | return collection; 72 | } 73 | 74 | @GetMapping("/automobiles/{id}") 75 | @ResponseStatus(HttpStatus.OK) 76 | //@Cacheable(value = "automobile", sync = true) 77 | //TODO: We do not have PERSON on the user map 78 | @PreAuthorize("hasRole('PERSON')") 79 | public Automobile getAutomobileById(@PathVariable Long id) { 80 | log.info("getAutomobileById() - start: id = {}", id); 81 | Automobile receivedAutomobile = repository.findById(id) 82 | //.orElseThrow(() -> new EntityNotFoundException("Automobile not found with id = " + id)); 83 | .orElseThrow(ThereIsNoSuchAutoException::new); 84 | if (receivedAutomobile.getDeleted()) { 85 | throw new AutoWasDeletedException(); 86 | } 87 | log.info("getAutomobileById() - end: Automobile = {}", receivedAutomobile.getId()); 88 | return receivedAutomobile; 89 | } 90 | 91 | @Hidden 92 | @GetMapping(value = "/automobiles", params = {"name"}) 93 | @ResponseStatus(HttpStatus.OK) 94 | public Collection findAutomobileByName(@RequestParam(value = "name") String name) { 95 | log.info("findAutomobileByName() - start: name = {}", name); 96 | Collection collection = repository.findByName(name); 97 | log.info("findAutomobileByName() - end: collection = {}", collection); 98 | return collection; 99 | } 100 | 101 | @PutMapping("/automobiles/{id}") 102 | @ResponseStatus(HttpStatus.OK) 103 | //@CachePut(value = "automobile", key = "#id") 104 | public Automobile refreshAutomobile(@PathVariable Long id, @RequestBody Automobile automobile) { 105 | log.info("refreshAutomobile() - start: id = {}, automobile = {}", id, automobile); 106 | Automobile updatedAutomobile = repository.findById(id) 107 | .map(entity -> { 108 | entity.checkColor(automobile); 109 | entity.setName(automobile.getName()); 110 | entity.setColor(automobile.getColor()); 111 | entity.setUpdateDate(automobile.getUpdateDate()); 112 | if (entity.getDeleted()) { 113 | throw new AutoWasDeletedException(); 114 | } 115 | return repository.save(entity); 116 | }) 117 | //.orElseThrow(() -> new EntityNotFoundException("Automobile not found with id = " + id)); 118 | .orElseThrow(ThereIsNoSuchAutoException::new); 119 | log.info("refreshAutomobile() - end: updatedAutomobile = {}", updatedAutomobile); 120 | return updatedAutomobile; 121 | } 122 | 123 | @DeleteMapping("/automobiles/{id}") 124 | @ResponseStatus(HttpStatus.NO_CONTENT) 125 | @CacheEvict(value = "automobile", key = "#id") 126 | public String removeAutomobileById(@PathVariable Long id) { 127 | log.info("removeAutomobileById() - start: id = {}", id); 128 | Automobile deletedAutomobile = repository.findById(id) 129 | .orElseThrow(ThereIsNoSuchAutoException::new); 130 | deletedAutomobile.setDeleted(Boolean.TRUE); 131 | repository.save(deletedAutomobile); 132 | log.info("removeAutomobileById() - end: id = {}", id); 133 | return "Deleted"; 134 | } 135 | 136 | @Hidden 137 | @DeleteMapping("/automobiles") 138 | @ResponseStatus(HttpStatus.NO_CONTENT) 139 | public void removeAllAutomobiles() { 140 | log.info("removeAllAutomobiles() - start"); 141 | repository.deleteAll(); 142 | log.info("removeAllAutomobiles() - end"); 143 | } 144 | 145 | @GetMapping(value = "/automobiles", params = {"color"}) 146 | @ResponseStatus(HttpStatus.OK) 147 | public Collection findAutomobileByColor( 148 | @Parameter(description = "Name of the Automobile to be obtained. Cannot be empty.", required = true) 149 | @RequestParam(value = "color") String color) { 150 | Instant start = Instant.now(); 151 | log.info("findAutomobileByColor() - start: time = {}", start); 152 | log.info("findAutomobileByColor() - start: color = {}", color); 153 | Collection collection = repository.findByColor(color); 154 | Instant end = Instant.now(); 155 | log.info("findAutomobileByColor() - end: milliseconds = {}", getTiming(start, end)); 156 | log.info("findAutomobileByColor() - end: collection = {}", collection); 157 | return collection; 158 | } 159 | 160 | @GetMapping(value = "/automobiles", params = {"name", "color"}) 161 | @ResponseStatus(HttpStatus.OK) 162 | public Collection findAutomobileByNameAndColor( 163 | @Parameter(description = "Name of the Automobile to be obtained. Cannot be empty.", required = true) 164 | @RequestParam(value = "name") String name, @RequestParam(value = "color") String color) { 165 | log.info("findAutomobileByNameAndColor() - start: name = {}, color = {}", name, color); 166 | Collection collection = repository.findByNameAndColor(name, color); 167 | log.info("findAutomobileByNameAndColor() - end: collection = {}", collection); 168 | return collection; 169 | } 170 | 171 | @GetMapping(value = "/automobiles", params = {"colorStartsWith"}) 172 | @ResponseStatus(HttpStatus.OK) 173 | public Collection findAutomobileByColorStartsWith( 174 | @RequestParam(value = "colorStartsWith") String colorStartsWith, 175 | @RequestParam(value = "page") int page, 176 | @RequestParam(value = "size") int size) { 177 | log.info("findAutomobileByColorStartsWith() - start: color = {}", colorStartsWith); 178 | Collection collection = repository 179 | .findByColorStartsWith(colorStartsWith, PageRequest.of(page, size, Sort.by("color"))); 180 | log.info("findAutomobileByColorStartsWith() - end: collection = {}", collection); 181 | return collection; 182 | } 183 | 184 | @GetMapping("/automobiles-names") 185 | @ResponseStatus(HttpStatus.OK) 186 | public List getAllAutomobilesByName() { 187 | log.info("getAllAutomobilesByName() - start"); 188 | List collection = repository.findAll(); 189 | List collectionName = collection.stream() 190 | .map(Automobile::getName) 191 | .sorted() 192 | .collect(Collectors.toList()); 193 | log.info("getAllAutomobilesByName() - end"); 194 | return collectionName; 195 | } 196 | 197 | @Override 198 | @PostMapping("/message") 199 | @ResponseStatus(HttpStatus.CREATED) 200 | public ResponseEntity pushMessage(@RequestBody Automobile automobile) { 201 | try { 202 | Topic autoTopic = Objects.requireNonNull(jmsTemplate 203 | .getConnectionFactory()).createConnection().createSession().createTopic("AutoTopic"); 204 | Automobile savedAutomobile = repository.save(automobile); 205 | log.info("\u001B[32m" + "Sending Automobile with id: " + savedAutomobile.getId() + "\u001B[0m"); 206 | jmsTemplate.convertAndSend(autoTopic, savedAutomobile); 207 | return new ResponseEntity<>(savedAutomobile, HttpStatus.OK); 208 | } catch (Exception exception) { 209 | return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR); 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/main/java/com/kaluzny/demo/web/JMSPublisher.java: -------------------------------------------------------------------------------- 1 | package com.kaluzny.demo.web; 2 | 3 | import com.kaluzny.demo.domain.Automobile; 4 | import org.springframework.http.ResponseEntity; 5 | 6 | public interface JMSPublisher { 7 | 8 | ResponseEntity pushMessage(Automobile automobile); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | # Spring Boot configuration 2 | spring: 3 | application: 4 | name: App 5 | profiles: 6 | active: development 7 | #main: 8 | # allow-bean-definition-overriding: true 9 | # Database 10 | datasource: 11 | driver-class-name: org.postgresql.Driver 12 | # For correct works with docker-compose, we need to change "localhost" to a service name, take from docker-compose.yml 13 | #url: jdbc:postgresql://db:5432/automobiles 14 | url: jdbc:postgresql://localhost:5432/automobiles 15 | username: postgres 16 | password: postgres 17 | # JPA properties 18 | jpa: 19 | hibernate: 20 | ddl-auto: update # When you launch the application for the first time - switch "update" at "create" 21 | show-sql: true 22 | database: postgresql 23 | database-platform: org.hibernate.dialect.PostgreSQLDialect 24 | #open-in-view: false 25 | #generate-ddl: true 26 | # Keycloak Configuration 27 | security: 28 | oauth2: 29 | resource-server: 30 | jwt: 31 | issuer-uri: http://localhost:8180/realms/automobile-realm 32 | # JMS configuration 33 | jms: 34 | pub-sub-domain: true 35 | activemq: 36 | broker-url: tcp://localhost:61616 37 | # Server configuration 38 | server: 39 | port: 8080 #set your port 40 | servlet: 41 | context-path: /demo 42 | # Logger configuration 43 | logging: 44 | pattern: 45 | console: "%d %-5level %logger : %msg%n" 46 | level: 47 | org.springframework: info 48 | org.springframework.security: debug 49 | org.springframework.security.oauth2: debug 50 | #org.hibernate: debug 51 | # Swagger configuration 52 | springdoc: 53 | swagger-ui: 54 | path: /swagger-ui.html # swagger-ui custom path 55 | api-docs: 56 | path: /v3/api-docs.yaml 57 | # spring actuator 58 | management: 59 | endpoints: 60 | #enabled-by-default: true # If changed to false, you can enable separate functionality as indicated below 61 | #endpoint: # here 62 | # health: 63 | # enabled: true 64 | web: 65 | exposure: 66 | # exclude: "*" 67 | include: "*" 68 | -------------------------------------------------------------------------------- /src/main/resources/body.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Ferrari", 3 | "color": "Red" 4 | } --------------------------------------------------------------------------------