├── settings.gradle ├── .gitignore ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .idea ├── vcs.xml ├── jpa-buddy.xml ├── modules │ ├── crazy-chat.main.iml │ └── crazy-chat.iml ├── .gitignore ├── misc.xml ├── aws.xml ├── gradle.xml ├── checkstyle-idea.xml ├── compiler.xml ├── jarRepositories.xml ├── libraries-with-intellij-classes.xml └── uiDesigner.xml ├── src ├── main │ ├── java │ │ └── may │ │ │ └── code │ │ │ └── crazy_chat │ │ │ ├── api │ │ │ ├── RandomIdGenerator.java │ │ │ ├── dto │ │ │ │ ├── ParticipantDto.java │ │ │ │ ├── ChatDto.java │ │ │ │ ├── ErrorDTO.java │ │ │ │ └── MessageDto.java │ │ │ ├── domains │ │ │ │ ├── Participant.java │ │ │ │ └── Chat.java │ │ │ ├── factories │ │ │ │ ├── ParticipantDtoFactory.java │ │ │ │ └── ChatDtoFactory.java │ │ │ ├── controllers │ │ │ │ ├── exceptions │ │ │ │ │ ├── CustomExceptionHandler.java │ │ │ │ │ └── CustomErrorController.java │ │ │ │ ├── rest │ │ │ │ │ ├── ChatRestController.java │ │ │ │ │ └── ParticipantRestController.java │ │ │ │ └── ws │ │ │ │ │ ├── ParticipantWsController.java │ │ │ │ │ └── ChatWsController.java │ │ │ └── services │ │ │ │ ├── ChatService.java │ │ │ │ └── ParticipantService.java │ │ │ ├── Application.java │ │ │ └── config │ │ │ ├── WebConfig.java │ │ │ ├── WebSocketConfig.java │ │ │ └── RedisConfig.java │ └── resources │ │ └── application.yml └── test │ ├── resources │ └── application-test.yml │ └── java │ └── may │ └── code │ └── crazy_chat │ └── api │ └── controllers │ └── ws │ └── WebSocketTests.java ├── gradlew.bat └── gradlew /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'crazy-chat' 2 | 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project exclude paths 2 | /.gradle/ 3 | /build/ 4 | /build/classes/java/main/ 5 | /build/classes/java/test/ -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FriMay/crazy-chat/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/jpa-buddy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/modules/crazy-chat.main.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /../../../../../:\Projects\Arbina\crazy-chat\.idea/dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | -------------------------------------------------------------------------------- /src/main/java/may/code/crazy_chat/api/RandomIdGenerator.java: -------------------------------------------------------------------------------- 1 | package may.code.crazy_chat.api; 2 | 3 | import java.util.UUID; 4 | 5 | public class RandomIdGenerator { 6 | 7 | public static String generate() { 8 | return UUID.randomUUID().toString().substring(0, 4); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/test/resources/application-test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | spring: 4 | datasource: 5 | driver-class-name: org.h2.Driver 6 | url: jdbc:h2:mem:test-db;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE 7 | jpa: 8 | hibernate: 9 | ddl-auto: update 10 | show-sql: false 11 | properties: 12 | hibernate: 13 | enable_lazy_load_no_trans: true -------------------------------------------------------------------------------- /.idea/modules/crazy-chat.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /src/main/java/may/code/crazy_chat/api/dto/ParticipantDto.java: -------------------------------------------------------------------------------- 1 | package may.code.crazy_chat.api.dto; 2 | 3 | import lombok.*; 4 | import lombok.experimental.FieldDefaults; 5 | 6 | import java.time.Instant; 7 | 8 | @Data 9 | @Builder 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | @FieldDefaults(level = AccessLevel.PRIVATE) 13 | public class ParticipantDto { 14 | 15 | String id; 16 | 17 | Instant enterAt; 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/may/code/crazy_chat/api/dto/ChatDto.java: -------------------------------------------------------------------------------- 1 | package may.code.crazy_chat.api.dto; 2 | 3 | import lombok.*; 4 | import lombok.experimental.FieldDefaults; 5 | import lombok.experimental.SuperBuilder; 6 | 7 | import java.time.Instant; 8 | 9 | @Data 10 | @Builder 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | @FieldDefaults(level = AccessLevel.PRIVATE) 14 | public class ChatDto { 15 | 16 | String id; 17 | 18 | String name; 19 | 20 | Instant createdAt; 21 | } 22 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | spring: 4 | rabbitmq: 5 | host: localhost 6 | username: guest 7 | password: guest 8 | # datasource: 9 | # driver-class-name: org.h2.Driver 10 | # url: jdbc:h2:mem:crazay-chat-db;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE 11 | # jpa: 12 | # hibernate: 13 | # ddl-auto: update 14 | # show-sql: false 15 | # properties: 16 | # hibernate: 17 | # enable_lazy_load_no_trans: true -------------------------------------------------------------------------------- /src/main/java/may/code/crazy_chat/api/dto/ErrorDTO.java: -------------------------------------------------------------------------------- 1 | package may.code.crazy_chat.api.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import lombok.*; 5 | import lombok.experimental.FieldDefaults; 6 | 7 | @Data 8 | @Builder 9 | @NoArgsConstructor 10 | @AllArgsConstructor 11 | @FieldDefaults(level = AccessLevel.PRIVATE) 12 | public class ErrorDTO { 13 | 14 | String error; 15 | 16 | @JsonProperty("error_description") 17 | String errorDescription; 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/may/code/crazy_chat/api/dto/MessageDto.java: -------------------------------------------------------------------------------- 1 | package may.code.crazy_chat.api.dto; 2 | 3 | import lombok.*; 4 | import lombok.experimental.FieldDefaults; 5 | 6 | import java.time.Instant; 7 | 8 | @Data 9 | @Builder 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | @FieldDefaults(level = AccessLevel.PRIVATE) 13 | public class MessageDto { 14 | 15 | String from; 16 | 17 | String message; 18 | 19 | @Builder.Default 20 | Instant createdAt = Instant.now(); 21 | } 22 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/aws.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 16 | 17 | -------------------------------------------------------------------------------- /src/main/java/may/code/crazy_chat/Application.java: -------------------------------------------------------------------------------- 1 | package may.code.crazy_chat; 2 | 3 | import org.springframework.boot.Banner; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.builder.SpringApplicationBuilder; 6 | 7 | @SpringBootApplication 8 | public class Application { 9 | 10 | public static void main(String[] args) { 11 | new SpringApplicationBuilder() 12 | .bannerMode(Banner.Mode.OFF) 13 | .sources(Application.class) 14 | .run(args); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/may/code/crazy_chat/api/domains/Participant.java: -------------------------------------------------------------------------------- 1 | package may.code.crazy_chat.api.domains; 2 | 3 | import lombok.*; 4 | import lombok.experimental.FieldDefaults; 5 | 6 | import java.io.Serializable; 7 | import java.time.Instant; 8 | 9 | @Data 10 | @Builder 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | @FieldDefaults(level = AccessLevel.PRIVATE) 14 | public class Participant implements Serializable { 15 | 16 | @Builder.Default 17 | Long enterAt = Instant.now().toEpochMilli(); 18 | 19 | String id; 20 | 21 | String sessionId; 22 | 23 | String chatId; 24 | } 25 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 16 | 17 | -------------------------------------------------------------------------------- /src/main/java/may/code/crazy_chat/api/factories/ParticipantDtoFactory.java: -------------------------------------------------------------------------------- 1 | package may.code.crazy_chat.api.factories; 2 | 3 | import may.code.crazy_chat.api.domains.Participant; 4 | import may.code.crazy_chat.api.dto.ParticipantDto; 5 | import org.springframework.stereotype.Component; 6 | 7 | import java.time.Instant; 8 | 9 | @Component 10 | public class ParticipantDtoFactory { 11 | 12 | public ParticipantDto makeParticipantDto(Participant participant) { 13 | return ParticipantDto.builder() 14 | .id(participant.getId()) 15 | .enterAt(Instant.ofEpochMilli(participant.getEnterAt())) 16 | .build(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.idea/checkstyle-idea.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 16 | -------------------------------------------------------------------------------- /src/main/java/may/code/crazy_chat/api/domains/Chat.java: -------------------------------------------------------------------------------- 1 | package may.code.crazy_chat.api.domains; 2 | 3 | import lombok.*; 4 | import lombok.experimental.FieldDefaults; 5 | import may.code.crazy_chat.api.RandomIdGenerator; 6 | 7 | import java.io.Serializable; 8 | import java.time.Instant; 9 | import java.util.UUID; 10 | 11 | @Data 12 | @Builder 13 | @NoArgsConstructor 14 | @AllArgsConstructor 15 | @FieldDefaults(level = AccessLevel.PRIVATE) 16 | public class Chat implements Serializable { 17 | 18 | @Builder.Default 19 | String id = RandomIdGenerator.generate(); 20 | 21 | String name; 22 | 23 | @Builder.Default 24 | Long createdAt = Instant.now().toEpochMilli(); 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/may/code/crazy_chat/config/WebConfig.java: -------------------------------------------------------------------------------- 1 | package may.code.crazy_chat.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.web.servlet.config.annotation.CorsRegistry; 5 | import org.springframework.web.servlet.config.annotation.EnableWebMvc; 6 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; 7 | 8 | @Configuration 9 | @EnableWebMvc 10 | public class WebConfig extends WebMvcConfigurerAdapter { 11 | 12 | @Override 13 | public void addCorsMappings(CorsRegistry registry) { 14 | registry.addMapping("/**") 15 | .allowedMethods("HEAD", "GET", "PUT", "POST", "DELETE", "PATCH"); 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/main/java/may/code/crazy_chat/api/factories/ChatDtoFactory.java: -------------------------------------------------------------------------------- 1 | package may.code.crazy_chat.api.factories; 2 | 3 | import may.code.crazy_chat.api.domains.Chat; 4 | import may.code.crazy_chat.api.domains.Participant; 5 | import may.code.crazy_chat.api.dto.ChatDto; 6 | import may.code.crazy_chat.api.dto.ParticipantDto; 7 | import org.springframework.stereotype.Component; 8 | 9 | import java.time.Instant; 10 | 11 | @Component 12 | public class ChatDtoFactory { 13 | 14 | public ChatDto makeChatDto(Chat chat) { 15 | return ChatDto.builder() 16 | .id(chat.getId()) 17 | .name(chat.getName()) 18 | .createdAt(Instant.ofEpochMilli(chat.getCreatedAt())) 19 | .build(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /src/main/java/may/code/crazy_chat/api/controllers/exceptions/CustomExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package may.code.crazy_chat.api.controllers.exceptions; 2 | 3 | import lombok.extern.log4j.Log4j2; 4 | import org.springframework.http.ResponseEntity; 5 | import org.springframework.web.bind.annotation.ControllerAdvice; 6 | import org.springframework.web.bind.annotation.ExceptionHandler; 7 | import org.springframework.web.context.request.WebRequest; 8 | import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; 9 | 10 | @Log4j2 11 | @ControllerAdvice 12 | public class CustomExceptionHandler extends ResponseEntityExceptionHandler { 13 | 14 | @ExceptionHandler(Exception.class) 15 | public ResponseEntity exception(Exception ex, WebRequest request) throws Exception { 16 | 17 | log.error("Exception during execution of application", ex); 18 | 19 | return handleException(ex, request); 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/java/may/code/crazy_chat/api/controllers/rest/ChatRestController.java: -------------------------------------------------------------------------------- 1 | package may.code.crazy_chat.api.controllers.rest; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.experimental.FieldDefaults; 6 | import lombok.extern.log4j.Log4j2; 7 | import may.code.crazy_chat.api.dto.ChatDto; 8 | import may.code.crazy_chat.api.factories.ChatDtoFactory; 9 | import may.code.crazy_chat.api.services.ChatService; 10 | import org.springframework.http.MediaType; 11 | import org.springframework.web.bind.annotation.GetMapping; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | import java.util.List; 15 | import java.util.stream.Collectors; 16 | 17 | @Log4j2 18 | @RequiredArgsConstructor 19 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 20 | @RestController 21 | public class ChatRestController { 22 | 23 | ChatService chatService; 24 | 25 | ChatDtoFactory chatDtoFactory; 26 | 27 | public static final String FETCH_CHATS = "/api/chats"; 28 | 29 | @GetMapping(value = FETCH_CHATS, produces = MediaType.APPLICATION_JSON_VALUE) 30 | public List fetchChats() { 31 | return chatService 32 | .getChats() 33 | .map(chatDtoFactory::makeChatDto) 34 | .collect(Collectors.toList()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/may/code/crazy_chat/api/controllers/ws/ParticipantWsController.java: -------------------------------------------------------------------------------- 1 | package may.code.crazy_chat.api.controllers.ws; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.experimental.FieldDefaults; 6 | import may.code.crazy_chat.api.dto.ParticipantDto; 7 | import org.springframework.messaging.simp.annotation.SubscribeMapping; 8 | import org.springframework.stereotype.Controller; 9 | 10 | @RequiredArgsConstructor 11 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 12 | @Controller 13 | public class ParticipantWsController { 14 | 15 | public static final String FETCH_PARTICIPANT_JOIN_IN_CHAT = "/topic/chats.{chat_id}.participants.join"; 16 | public static final String FETCH_PARTICIPANT_LEAVE_FROM_CHAT = "/topic/chats.{chat_id}.participants.leave"; 17 | 18 | @SubscribeMapping(FETCH_PARTICIPANT_JOIN_IN_CHAT) 19 | public ParticipantDto fetchParticipantJoinInChat() { 20 | return null; 21 | } 22 | 23 | @SubscribeMapping(FETCH_PARTICIPANT_LEAVE_FROM_CHAT) 24 | public ParticipantDto fetchParticipantLeaveFromChat() { 25 | return null; 26 | } 27 | 28 | public static String getFetchParticipantJoinInChatDestination(String chatId) { 29 | return FETCH_PARTICIPANT_JOIN_IN_CHAT.replace("{chat_id}", chatId); 30 | } 31 | 32 | public static String getFetchParticipantLeaveFromChatDestination(String chatId) { 33 | return FETCH_PARTICIPANT_LEAVE_FROM_CHAT.replace("{chat_id}", chatId); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/may/code/crazy_chat/api/controllers/rest/ParticipantRestController.java: -------------------------------------------------------------------------------- 1 | package may.code.crazy_chat.api.controllers.rest; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.experimental.FieldDefaults; 6 | import lombok.extern.log4j.Log4j2; 7 | import may.code.crazy_chat.api.dto.ParticipantDto; 8 | import may.code.crazy_chat.api.factories.ParticipantDtoFactory; 9 | import may.code.crazy_chat.api.services.ParticipantService; 10 | import org.springframework.http.MediaType; 11 | import org.springframework.web.bind.annotation.GetMapping; 12 | import org.springframework.web.bind.annotation.PathVariable; 13 | import org.springframework.web.bind.annotation.RestController; 14 | 15 | import java.util.List; 16 | import java.util.stream.Collectors; 17 | 18 | @Log4j2 19 | @RequiredArgsConstructor 20 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 21 | @RestController 22 | public class ParticipantRestController { 23 | 24 | ParticipantService participantService; 25 | 26 | ParticipantDtoFactory participantDtoFactory; 27 | 28 | public static final String FETCH_PARTICIPANTS = "/api/chats/{chat_id}/participants"; 29 | 30 | @GetMapping(value = FETCH_PARTICIPANTS, produces = MediaType.APPLICATION_JSON_VALUE) 31 | public List fetchParticipants(@PathVariable("chat_id") String chatId) { 32 | return participantService 33 | .getParticipants(chatId) 34 | .map(participantDtoFactory::makeParticipantDto) 35 | .collect(Collectors.toList()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/may/code/crazy_chat/api/controllers/exceptions/CustomErrorController.java: -------------------------------------------------------------------------------- 1 | package may.code.crazy_chat.api.controllers.exceptions; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.experimental.FieldDefaults; 6 | import may.code.crazy_chat.api.dto.ErrorDTO; 7 | import org.springframework.boot.web.error.ErrorAttributeOptions; 8 | import org.springframework.boot.web.servlet.error.ErrorAttributes; 9 | import org.springframework.boot.web.servlet.error.ErrorController; 10 | import org.springframework.http.ResponseEntity; 11 | import org.springframework.stereotype.Controller; 12 | import org.springframework.web.bind.annotation.RequestMapping; 13 | import org.springframework.web.context.request.WebRequest; 14 | 15 | import java.util.Map; 16 | 17 | @RequiredArgsConstructor 18 | @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) 19 | @Controller 20 | public class CustomErrorController implements ErrorController { 21 | 22 | private static final String PATH = "/error"; 23 | 24 | ErrorAttributes errorAttributes; 25 | 26 | public String getErrorPath() { 27 | return PATH; 28 | } 29 | 30 | @RequestMapping(CustomErrorController.PATH) 31 | public ResponseEntity error(WebRequest webRequest) { 32 | 33 | Map attributes = errorAttributes.getErrorAttributes( 34 | webRequest, 35 | ErrorAttributeOptions.of(ErrorAttributeOptions.Include.EXCEPTION, ErrorAttributeOptions.Include.MESSAGE) 36 | ); 37 | 38 | return ResponseEntity 39 | .status((Integer) attributes.get("status")) 40 | .body(ErrorDTO 41 | .builder() 42 | .error((String) attributes.get("error")) 43 | .errorDescription((String) attributes.get("message")) 44 | .build() 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/may/code/crazy_chat/config/WebSocketConfig.java: -------------------------------------------------------------------------------- 1 | package may.code.crazy_chat.config; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.experimental.FieldDefaults; 6 | import lombok.experimental.NonFinal; 7 | import lombok.extern.log4j.Log4j2; 8 | import org.springframework.beans.factory.annotation.Value; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.messaging.simp.config.MessageBrokerRegistry; 11 | import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; 12 | import org.springframework.web.socket.config.annotation.StompEndpointRegistry; 13 | import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; 14 | 15 | @Log4j2 16 | @RequiredArgsConstructor 17 | @FieldDefaults(level = AccessLevel.PRIVATE) 18 | @Configuration 19 | @EnableWebSocketMessageBroker 20 | public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { 21 | 22 | @Value("${spring.rabbitmq.host}") 23 | String host; 24 | 25 | @Value("${spring.rabbitmq.username}") 26 | String username; 27 | 28 | @Value("${spring.rabbitmq.password}") 29 | String password; 30 | 31 | public static final String TOPIC_DESTINATION_PREFIX = "/topic/"; 32 | public static final String REGISTRY = "/ws"; 33 | 34 | @Override 35 | public void registerStompEndpoints(StompEndpointRegistry registry) { 36 | registry.addEndpoint(REGISTRY) 37 | .withSockJS(); 38 | } 39 | 40 | @Override 41 | public void configureMessageBroker(MessageBrokerRegistry config) { 42 | config.enableStompBrokerRelay(TOPIC_DESTINATION_PREFIX) 43 | .setRelayHost(host) 44 | .setClientLogin(username) 45 | .setClientPasscode(password) 46 | .setSystemLogin(username) 47 | .setSystemPasscode(password); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/may/code/crazy_chat/api/services/ChatService.java: -------------------------------------------------------------------------------- 1 | package may.code.crazy_chat.api.services; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.experimental.FieldDefaults; 6 | import lombok.extern.log4j.Log4j2; 7 | import may.code.crazy_chat.api.domains.Chat; 8 | import may.code.crazy_chat.api.dto.ChatDto; 9 | import may.code.crazy_chat.api.controllers.ws.ChatWsController; 10 | import may.code.crazy_chat.api.factories.ChatDtoFactory; 11 | import org.springframework.data.redis.core.SetOperations; 12 | import org.springframework.messaging.simp.SimpMessagingTemplate; 13 | import org.springframework.stereotype.Service; 14 | 15 | import java.util.HashSet; 16 | import java.util.Objects; 17 | import java.util.Optional; 18 | import java.util.Set; 19 | import java.util.stream.Stream; 20 | 21 | @Log4j2 22 | @RequiredArgsConstructor 23 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 24 | @Service 25 | public class ChatService { 26 | 27 | ChatDtoFactory chatDtoFactory; 28 | 29 | SimpMessagingTemplate messagingTemplate; 30 | 31 | SetOperations setOperations; 32 | 33 | private static final String KEY = "may:code:crazy-chat:chats"; 34 | 35 | public void createChat(String chatName) { 36 | 37 | log.info(String.format("Chat \"%s\" created.", chatName)); 38 | 39 | Chat chat = Chat.builder() 40 | .name(chatName) 41 | .build(); 42 | 43 | setOperations.add(KEY, chat); 44 | 45 | messagingTemplate.convertAndSend( 46 | ChatWsController.FETCH_CREATE_CHAT_EVENT, 47 | chatDtoFactory.makeChatDto(chat) 48 | ); 49 | } 50 | 51 | public void deleteChat(String chatId) { 52 | 53 | getChats() 54 | .filter(chat -> Objects.equals(chatId, chat.getId())) 55 | .findAny() 56 | .ifPresent(chat -> { 57 | 58 | log.info(String.format("Chat \"%s\" deleted.", chat.getName())); 59 | 60 | setOperations.remove(KEY, chat); 61 | 62 | messagingTemplate.convertAndSend( 63 | ChatWsController.FETCH_DELETE_CHAT_EVENT, 64 | chatDtoFactory.makeChatDto(chat) 65 | ); 66 | }); 67 | } 68 | 69 | public Stream getChats() { 70 | return Optional 71 | .ofNullable(setOperations.members(KEY)) 72 | .orElseGet(HashSet::new) 73 | .stream(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/may/code/crazy_chat/config/RedisConfig.java: -------------------------------------------------------------------------------- 1 | package may.code.crazy_chat.config; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.experimental.FieldDefaults; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.data.redis.connection.RedisStandaloneConfiguration; 9 | import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; 10 | import org.springframework.data.redis.core.HashOperations; 11 | import org.springframework.data.redis.core.RedisTemplate; 12 | import org.springframework.data.redis.core.SetOperations; 13 | import org.springframework.data.redis.core.ZSetOperations; 14 | import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; 15 | 16 | @FieldDefaults(level = AccessLevel.PRIVATE) 17 | @Configuration 18 | public class RedisConfig { 19 | 20 | @Value("${redis.database:-1}") 21 | Integer redisDatabase; 22 | 23 | @Value("${redis.password:}") 24 | String password; 25 | 26 | @Value("${redis.host:localhost}") 27 | String host; 28 | 29 | @Value("${redis.port:6379}") 30 | Integer port; 31 | 32 | @Bean 33 | public JedisConnectionFactory jedisConnectionFactory() { 34 | 35 | RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(host, port); 36 | 37 | if (!password.trim().isEmpty()) { 38 | redisStandaloneConfiguration.setPassword(password); 39 | } 40 | 41 | if (redisDatabase > 0) { 42 | redisStandaloneConfiguration.setDatabase(redisDatabase); 43 | } 44 | 45 | return new JedisConnectionFactory(redisStandaloneConfiguration); 46 | } 47 | 48 | @Bean 49 | public RedisTemplate redisTemplate() { 50 | 51 | final RedisTemplate redisTemplate = new RedisTemplate<>(); 52 | 53 | redisTemplate.setConnectionFactory(jedisConnectionFactory()); 54 | redisTemplate.setDefaultSerializer(new GenericJackson2JsonRedisSerializer()); 55 | redisTemplate.afterPropertiesSet(); 56 | 57 | return redisTemplate; 58 | } 59 | 60 | @Bean 61 | public HashOperations hashOperations(RedisTemplate redisTemplate) { 62 | return redisTemplate.opsForHash(); 63 | } 64 | 65 | @Bean 66 | public SetOperations setOperations(RedisTemplate redisTemplate) { 67 | return redisTemplate.opsForSet(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /.idea/libraries-with-intellij-classes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 64 | 65 | -------------------------------------------------------------------------------- /src/main/java/may/code/crazy_chat/api/controllers/ws/ChatWsController.java: -------------------------------------------------------------------------------- 1 | package may.code.crazy_chat.api.controllers.ws; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.experimental.FieldDefaults; 6 | import may.code.crazy_chat.api.dto.ChatDto; 7 | import may.code.crazy_chat.api.dto.MessageDto; 8 | import may.code.crazy_chat.api.services.ChatService; 9 | import may.code.crazy_chat.api.services.ParticipantService; 10 | import org.springframework.messaging.handler.annotation.DestinationVariable; 11 | import org.springframework.messaging.handler.annotation.Header; 12 | import org.springframework.messaging.handler.annotation.MessageMapping; 13 | import org.springframework.messaging.simp.SimpMessagingTemplate; 14 | import org.springframework.messaging.simp.annotation.SubscribeMapping; 15 | import org.springframework.stereotype.Controller; 16 | 17 | @RequiredArgsConstructor 18 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 19 | @Controller 20 | public class ChatWsController { 21 | 22 | ChatService chatService; 23 | 24 | ParticipantService participantService; 25 | 26 | SimpMessagingTemplate messagingTemplate; 27 | 28 | public static final String CREATE_CHAT = "/topic/chats.create"; 29 | 30 | public static final String FETCH_CREATE_CHAT_EVENT = "/topic/chats.create.event"; 31 | public static final String FETCH_DELETE_CHAT_EVENT = "/topic/chats.delete.event"; 32 | 33 | public static final String SEND_MESSAGE_TO_ALL = "/topic/chats.{chat_id}.messages.send"; 34 | public static final String SEND_MESSAGE_TO_PARTICIPANT = "/topic/chats.{chat_id}.participants.{participant_id}.messages.send"; 35 | 36 | public static final String FETCH_MESSAGES = "/topic/chats.{chat_id}.messages"; 37 | public static final String FETCH_PERSONAL_MESSAGES = "/topic/chats.{chat_id}.participants.{participant_id}"; 38 | 39 | @MessageMapping(CREATE_CHAT) 40 | public void createChat(String chatName) { 41 | chatService.createChat(chatName); 42 | } 43 | 44 | @SubscribeMapping(FETCH_CREATE_CHAT_EVENT) 45 | public ChatDto fetchCreateChatEvent() { 46 | return null; 47 | } 48 | 49 | @SubscribeMapping(FETCH_DELETE_CHAT_EVENT) 50 | public ChatDto fetchDeleteChatEvent() { 51 | return null; 52 | } 53 | 54 | @MessageMapping(SEND_MESSAGE_TO_ALL) 55 | public void sendMessageToAll( 56 | @DestinationVariable("chat_id") String chatId, 57 | String message, 58 | @Header String simpSessionId) { 59 | 60 | sendMessage( 61 | getFetchMessagesDestination(chatId), 62 | simpSessionId, 63 | message 64 | ); 65 | } 66 | 67 | @MessageMapping(SEND_MESSAGE_TO_PARTICIPANT) 68 | public void sendMessageToParticipant( 69 | @DestinationVariable("chat_id") String chatId, 70 | @DestinationVariable("participant_id") String participantId, 71 | String message, 72 | @Header String simpSessionId) { 73 | 74 | sendMessage( 75 | getFetchPersonalMessagesDestination(chatId, participantId), 76 | simpSessionId, 77 | message 78 | ); 79 | } 80 | 81 | @SubscribeMapping(FETCH_MESSAGES) 82 | public MessageDto fetchMessages(@DestinationVariable("chat_id") String chatId) { 83 | return null; 84 | } 85 | 86 | @SubscribeMapping(FETCH_PERSONAL_MESSAGES) 87 | public MessageDto fetchPersonalMessages( 88 | @DestinationVariable("chat_id") String chatId, 89 | @DestinationVariable("participant_id") String participantId, 90 | @Header String simpSessionId) { 91 | 92 | participantService.handleJoinChat(simpSessionId, participantId, chatId); 93 | 94 | return null; 95 | } 96 | 97 | private void sendMessage(String destination, String sessionId, String message) { 98 | messagingTemplate.convertAndSend( 99 | destination, 100 | MessageDto.builder() 101 | .from(sessionId) 102 | .message(message) 103 | .build() 104 | ); 105 | } 106 | 107 | public static String getFetchMessagesDestination(String chatId) { 108 | return FETCH_MESSAGES.replace("{chat_id}", chatId); 109 | } 110 | 111 | public static String getFetchPersonalMessagesDestination(String chatId, String participantId) { 112 | return FETCH_PERSONAL_MESSAGES 113 | .replace("{chat_id}", chatId) 114 | .replace("{participant_id}", participantId); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/may/code/crazy_chat/api/services/ParticipantService.java: -------------------------------------------------------------------------------- 1 | package may.code.crazy_chat.api.services; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.experimental.FieldDefaults; 6 | import lombok.extern.log4j.Log4j2; 7 | import may.code.crazy_chat.api.domains.Participant; 8 | import may.code.crazy_chat.api.dto.ParticipantDto; 9 | import may.code.crazy_chat.api.controllers.ws.ParticipantWsController; 10 | import may.code.crazy_chat.api.factories.ParticipantDtoFactory; 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.context.event.EventListener; 13 | import org.springframework.data.redis.core.SetOperations; 14 | import org.springframework.messaging.simp.SimpMessageHeaderAccessor; 15 | import org.springframework.messaging.simp.SimpMessagingTemplate; 16 | import org.springframework.stereotype.Service; 17 | import org.springframework.web.socket.messaging.AbstractSubProtocolEvent; 18 | import org.springframework.web.socket.messaging.SessionDisconnectEvent; 19 | import org.springframework.web.socket.messaging.SessionSubscribeEvent; 20 | import org.springframework.web.socket.messaging.SessionUnsubscribeEvent; 21 | 22 | import java.util.HashSet; 23 | import java.util.Map; 24 | import java.util.Optional; 25 | import java.util.Set; 26 | import java.util.concurrent.ConcurrentHashMap; 27 | import java.util.stream.Stream; 28 | 29 | @Log4j2 30 | @RequiredArgsConstructor 31 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 32 | @Service 33 | public class ParticipantService { 34 | 35 | ChatService chatService; 36 | 37 | SimpMessagingTemplate messagingTemplate; 38 | 39 | ParticipantDtoFactory participantDtoFactory; 40 | 41 | private static final Map sessionIdToParticipantMap = new ConcurrentHashMap<>(); 42 | 43 | SetOperations setOperations; 44 | 45 | public void handleJoinChat(String sessionId, String participantId, String chatId) { 46 | 47 | log.info(String.format("Participant \"%s\" join in chat \"%s\".", sessionId, chatId)); 48 | 49 | Participant participant = Participant.builder() 50 | .sessionId(sessionId) 51 | .id(participantId) 52 | .chatId(chatId) 53 | .build(); 54 | 55 | sessionIdToParticipantMap.put(participant.getSessionId(), participant); 56 | 57 | setOperations.add(ParticipantKeyHelper.makeKey(chatId), participant); 58 | 59 | messagingTemplate.convertAndSend( 60 | ParticipantWsController.getFetchParticipantJoinInChatDestination(chatId), 61 | participantDtoFactory.makeParticipantDto(participant) 62 | ); 63 | } 64 | 65 | @EventListener 66 | public void handleUnsubscribe(SessionUnsubscribeEvent event) { 67 | handleLeaveChat(event); 68 | } 69 | 70 | @EventListener 71 | public void handleDisconnect(SessionDisconnectEvent event) { 72 | handleLeaveChat(event); 73 | } 74 | 75 | private void handleLeaveChat(AbstractSubProtocolEvent event) { 76 | 77 | SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.wrap(event.getMessage()); 78 | 79 | Optional 80 | .ofNullable(headerAccessor.getSessionId()) 81 | .map(sessionIdToParticipantMap::remove) 82 | .ifPresent(participant -> { 83 | 84 | String chatId = participant.getChatId(); 85 | 86 | log.info( 87 | String.format( 88 | "Participant \"%s\" leave from \"%s\" chat.", 89 | participant.getSessionId(), 90 | chatId 91 | ) 92 | ); 93 | 94 | String key = ParticipantKeyHelper.makeKey(chatId); 95 | 96 | setOperations.remove(key, participant); 97 | 98 | Optional 99 | .ofNullable(setOperations.size(key)) 100 | .filter(size -> size == 0L) 101 | .ifPresent(size -> chatService.deleteChat(chatId)); 102 | 103 | messagingTemplate.convertAndSend( 104 | key, 105 | participantDtoFactory.makeParticipantDto(participant) 106 | ); 107 | }); 108 | } 109 | 110 | public Stream getParticipants(String chatId) { 111 | return Optional 112 | .ofNullable(setOperations.members(ParticipantKeyHelper.makeKey(chatId))) 113 | .orElseGet(HashSet::new) 114 | .stream(); 115 | } 116 | 117 | private static class ParticipantKeyHelper { 118 | 119 | private static final String KEY = "may:code:crazy-chat:chats:{chat_id}:participants"; 120 | 121 | public static String makeKey(String chatId) { 122 | return KEY.replace("{chat_id}", chatId); 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /src/test/java/may/code/crazy_chat/api/controllers/ws/WebSocketTests.java: -------------------------------------------------------------------------------- 1 | package may.code.crazy_chat.api.controllers.ws; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import lombok.*; 5 | import lombok.experimental.FieldDefaults; 6 | import lombok.extern.log4j.Log4j2; 7 | import may.code.crazy_chat.api.RandomIdGenerator; 8 | import may.code.crazy_chat.api.controllers.rest.ChatRestController; 9 | import may.code.crazy_chat.api.dto.ChatDto; 10 | import may.code.crazy_chat.config.WebSocketConfig; 11 | import org.junit.jupiter.api.*; 12 | import org.junit.platform.runner.JUnitPlatform; 13 | import org.junit.runner.RunWith; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.beans.factory.annotation.Value; 16 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 17 | import org.springframework.boot.test.context.SpringBootTest; 18 | import org.springframework.lang.NonNull; 19 | import org.springframework.messaging.converter.MappingJackson2MessageConverter; 20 | import org.springframework.messaging.simp.stomp.StompFrameHandler; 21 | import org.springframework.messaging.simp.stomp.StompHeaders; 22 | import org.springframework.messaging.simp.stomp.StompSession; 23 | import org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter; 24 | import org.springframework.test.context.ActiveProfiles; 25 | import org.springframework.test.web.servlet.MockMvc; 26 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; 27 | import org.springframework.web.socket.client.standard.StandardWebSocketClient; 28 | import org.springframework.web.socket.messaging.WebSocketStompClient; 29 | import org.springframework.web.socket.sockjs.client.SockJsClient; 30 | import org.springframework.web.socket.sockjs.client.Transport; 31 | import org.springframework.web.socket.sockjs.client.WebSocketTransport; 32 | 33 | import java.lang.reflect.Type; 34 | import java.util.ArrayList; 35 | import java.util.LinkedHashMap; 36 | import java.util.List; 37 | import java.util.UUID; 38 | import java.util.concurrent.CompletableFuture; 39 | import java.util.concurrent.TimeUnit; 40 | 41 | import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; 42 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 43 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 44 | 45 | @Log4j2 46 | @ActiveProfiles("test-vlad") 47 | @RunWith(JUnitPlatform.class) 48 | @SpringBootTest(webEnvironment = RANDOM_PORT) 49 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 50 | @AutoConfigureMockMvc 51 | public class WebSocketTests { 52 | 53 | @Value("${local.server.port}") 54 | private int port; 55 | 56 | private static WebClient client; 57 | 58 | @Autowired 59 | ObjectMapper mapper; 60 | 61 | @Autowired 62 | MockMvc mockMvc; 63 | 64 | @BeforeAll 65 | public void setup() throws Exception { 66 | 67 | RunStopFrameHandler runStopFrameHandler = new RunStopFrameHandler(new CompletableFuture<>()); 68 | 69 | String wsUrl = "ws://127.0.0.1:" + port + WebSocketConfig.REGISTRY; 70 | 71 | WebSocketStompClient stompClient = new WebSocketStompClient(new SockJsClient(createTransportClient())); 72 | 73 | stompClient.setMessageConverter(new MappingJackson2MessageConverter()); 74 | 75 | StompSession stompSession = stompClient 76 | .connect(wsUrl, new StompSessionHandlerAdapter() {}) 77 | .get(1, TimeUnit.SECONDS); 78 | 79 | client = WebClient.builder() 80 | .stompClient(stompClient) 81 | .stompSession(stompSession) 82 | .handler(runStopFrameHandler) 83 | .build(); 84 | } 85 | 86 | @AfterAll 87 | public void tearDown() { 88 | 89 | if (client.getStompSession().isConnected()) { 90 | client.getStompSession().disconnect(); 91 | client.getStompClient().stop(); 92 | } 93 | } 94 | 95 | @SneakyThrows 96 | @Test 97 | public void should_PassSuccessfully_When_CreateChat() { 98 | 99 | StompSession stompSession = client.getStompSession(); 100 | 101 | RunStopFrameHandler handler = client.getHandler(); 102 | 103 | String chatName = "Crazy chat"; 104 | 105 | stompSession.send( 106 | ChatWsController.CREATE_CHAT, 107 | chatName 108 | ); 109 | 110 | String contentAsString = mockMvc 111 | .perform(MockMvcRequestBuilders.get(ChatRestController.FETCH_CHATS)) 112 | .andExpect(status().isOk()) 113 | .andReturn() 114 | .getResponse() 115 | .getContentAsString(); 116 | 117 | List> params = 118 | (List>) mapper.readValue(contentAsString, List.class); 119 | 120 | Assertions.assertFalse(params.isEmpty()); 121 | 122 | String chatId = (String) params.get(0).get("id"); 123 | 124 | String destination = ChatWsController.getFetchPersonalMessagesDestination(chatId, RandomIdGenerator.generate()); 125 | 126 | final RunStopFrameHandler runStopFrameHandler = new RunStopFrameHandler(new CompletableFuture<>()); 127 | stompSession.subscribe( 128 | destination, 129 | runStopFrameHandler 130 | ); 131 | } 132 | 133 | private List createTransportClient() { 134 | 135 | List transports = new ArrayList<>(1); 136 | 137 | transports.add(new WebSocketTransport(new StandardWebSocketClient())); 138 | 139 | return transports; 140 | } 141 | 142 | @Data 143 | @AllArgsConstructor 144 | @FieldDefaults(level = AccessLevel.PRIVATE) 145 | private class RunStopFrameHandler implements StompFrameHandler { 146 | 147 | CompletableFuture future; 148 | 149 | @Override 150 | public @NonNull Type getPayloadType(StompHeaders stompHeaders) { 151 | 152 | log.info(stompHeaders.toString()); 153 | 154 | return byte[].class; 155 | } 156 | 157 | @Override 158 | public void handleFrame(@NonNull StompHeaders stompHeaders, Object o) { 159 | 160 | log.info(o); 161 | 162 | future.complete(o); 163 | 164 | future = new CompletableFuture<>(); 165 | } 166 | } 167 | 168 | @Data 169 | @Builder 170 | @FieldDefaults(level = AccessLevel.PRIVATE) 171 | private static class WebClient { 172 | 173 | WebSocketStompClient stompClient; 174 | 175 | StompSession stompSession; 176 | 177 | String sessionToken; 178 | 179 | RunStopFrameHandler handler; 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /.idea/uiDesigner.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | --------------------------------------------------------------------------------