├── .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 | [](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 | 
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 | 
65 | *Keycloak — Create Initial Admin*
66 |
67 | Click Create and proceed to administration console and then login with your created initial admin.
68 |
69 | 
70 | *Keycloak — Log In Admin*
71 |
72 | The first screen Keycloak shows after login is the Master realm details.
73 |
74 | 
75 | *Keycloak — Master Realm Details*
76 |
77 | ### 3.1 Create a Realm
78 |
79 | Let’s name our first realm `automobile`:
80 |
81 | 
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 | 
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 | 
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 | 
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 | 
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 | 
130 | *Postman — Token Exchange Request*
131 |
132 | or generate new `access token` and `refresh token`
133 |
134 | 
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 | }
--------------------------------------------------------------------------------