diff --git a/docker-compose.yml b/docker-compose.yml index 7a56b2a..f9b75e8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,6 +8,22 @@ services: - MYSQL_PASSWORD=${DB_PASSWORD} ports: - "3302:3306" + + kafka: + image: wurstmeister/kafka + ports: + - "9092:9092" + environment: + - KAFKA_ADVERTISED_HOST_NAME=kafka + - KAFKA_ADVERTISED_PORT=9092 + - KAFKA_CREATE_TOPICS=topic1:1:1 + - KAFKA_ZOOKEEPER_CONNECT=${KAFKA_ZOOKEEPER_CONNECT} + + zookeeper: + image: wurstmeister/zookeeper + ports: + - "2181:2181" + app: build: context: . @@ -30,4 +46,5 @@ services: ports: - "8080:8080" depends_on: + - kafka - mysql diff --git a/pom.xml b/pom.xml index ee6261f..1cc2f4e 100644 --- a/pom.xml +++ b/pom.xml @@ -1,6 +1,6 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 org.springframework.boot @@ -74,6 +74,15 @@ firebase-admin 9.1.1 + + + org.springframework.boot + spring-boot-starter-websocket + + + org.springframework.kafka + spring-kafka + @@ -113,4 +122,4 @@ - + \ No newline at end of file diff --git a/src/main/java/com/rm/mynotes/model/Note.java b/src/main/java/com/rm/mynotes/model/Note.java index 259b2a9..463f74f 100644 --- a/src/main/java/com/rm/mynotes/model/Note.java +++ b/src/main/java/com/rm/mynotes/model/Note.java @@ -12,6 +12,8 @@ import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; @Data @Builder @@ -40,6 +42,8 @@ public class Note { @NotNull private OffsetDateTime lastUpdate; + private List reminders = new ArrayList<>(); + @NotNull private OffsetDateTime createdAt; @@ -53,5 +57,6 @@ public Note(NoteDTO noteDTO) { this.description = noteDTO.getDescription(); this.createdAt = CommonFunctions.getCurrentDatetime(); this.lastUpdate = CommonFunctions.getCurrentDatetime(); + this.reminders = new ArrayList<>(); } } diff --git a/src/main/java/com/rm/mynotes/model/Notification.java b/src/main/java/com/rm/mynotes/model/Notification.java new file mode 100644 index 0000000..e3e9610 --- /dev/null +++ b/src/main/java/com/rm/mynotes/model/Notification.java @@ -0,0 +1,34 @@ +package com.rm.mynotes.model; + +import com.rm.mynotes.utils.constants.StatusTypes; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; +import java.time.OffsetDateTime; +import java.util.Set; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Entity +public class Notification { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotNull + private OffsetDateTime createdAt; + + private Boolean wasRead = false; + + private Set content; + + @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) + @JoinTable(name = "notification_reminder", joinColumns = @JoinColumn(name = "notification_id", referencedColumnName = "id"), + inverseJoinColumns = @JoinColumn(name = "reminder_id", referencedColumnName = "id") + ) + private Reminder reminder; +} diff --git a/src/main/java/com/rm/mynotes/model/Reminder.java b/src/main/java/com/rm/mynotes/model/Reminder.java new file mode 100644 index 0000000..5337600 --- /dev/null +++ b/src/main/java/com/rm/mynotes/model/Reminder.java @@ -0,0 +1,35 @@ +package com.rm.mynotes.model; + +import com.rm.mynotes.utils.constants.StatusTypes; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; +import java.time.OffsetDateTime; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Entity +public class Reminder { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotNull + private String title; + + @NotNull + private OffsetDateTime createdAt; + + @NotNull + private OffsetDateTime lastUpdate; + + @NotNull + private OffsetDateTime reminderDate; + + @Enumerated(EnumType.STRING) + private StatusTypes status; +} diff --git a/src/main/java/com/rm/mynotes/repository/NotificationRepository.java b/src/main/java/com/rm/mynotes/repository/NotificationRepository.java new file mode 100644 index 0000000..5b666ae --- /dev/null +++ b/src/main/java/com/rm/mynotes/repository/NotificationRepository.java @@ -0,0 +1,9 @@ +package com.rm.mynotes.repository; + +import com.rm.mynotes.model.Notification; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface NotificationRepository extends JpaRepository { +} diff --git a/src/main/java/com/rm/mynotes/repository/ReminderRepository.java b/src/main/java/com/rm/mynotes/repository/ReminderRepository.java new file mode 100644 index 0000000..29e537f --- /dev/null +++ b/src/main/java/com/rm/mynotes/repository/ReminderRepository.java @@ -0,0 +1,9 @@ +package com.rm.mynotes.repository; + +import com.rm.mynotes.model.Reminder; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ReminderRepository extends JpaRepository { +} diff --git a/src/main/java/com/rm/mynotes/resource/ReminderResource.java b/src/main/java/com/rm/mynotes/resource/ReminderResource.java new file mode 100644 index 0000000..1e72c7d --- /dev/null +++ b/src/main/java/com/rm/mynotes/resource/ReminderResource.java @@ -0,0 +1,28 @@ +package com.rm.mynotes.resource; + +import com.rm.mynotes.model.Reminder; +import com.rm.mynotes.service.mold.ReminderService; +import com.rm.mynotes.utils.dto.requests.ReminderDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping +public class ReminderResource { + @Autowired + private ReminderService reminderService; + + @GetMapping("/app/reminder/1") + public String test() { + return "você fez uma requisição!"; + } + + @PostMapping("/app/reminder/{noteId}") + public ResponseEntity createReminder(Authentication authentication, @RequestBody ReminderDTO reminderDTO, @RequestParam(required = true, name = "noteId") Long noteId) { + return reminderService.createReminder(authentication, reminderDTO, noteId); + } +} diff --git a/src/main/java/com/rm/mynotes/resource/WebSocketResource.java b/src/main/java/com/rm/mynotes/resource/WebSocketResource.java new file mode 100644 index 0000000..50866bb --- /dev/null +++ b/src/main/java/com/rm/mynotes/resource/WebSocketResource.java @@ -0,0 +1,21 @@ +package com.rm.mynotes.resource; + +import com.rm.mynotes.model.Reminder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.SendTo; +import org.springframework.stereotype.Controller; + +@Controller +public class WebSocketResource { + @Autowired + private KafkaTemplate kafkaTemplate; + + @MessageMapping("/createReminder") + @SendTo("/topic/reminders") + public Reminder createEvent(Reminder reminder) { + kafkaTemplate.send("remindersTopic", reminder.toString()); + return reminder; + } +} diff --git a/src/main/java/com/rm/mynotes/service/impl/ReminderServiceImplementation.java b/src/main/java/com/rm/mynotes/service/impl/ReminderServiceImplementation.java new file mode 100644 index 0000000..50e3397 --- /dev/null +++ b/src/main/java/com/rm/mynotes/service/impl/ReminderServiceImplementation.java @@ -0,0 +1,71 @@ +package com.rm.mynotes.service.impl; + +import com.rm.mynotes.model.Note; +import com.rm.mynotes.model.Reminder; +import com.rm.mynotes.model.UserEntity; +import com.rm.mynotes.repository.NoteRepository; +import com.rm.mynotes.repository.ReminderRepository; +import com.rm.mynotes.repository.UserRepository; +import com.rm.mynotes.service.mold.ReminderService; +import com.rm.mynotes.utils.constants.RoutePaths; +import com.rm.mynotes.utils.constants.StatusTypes; +import com.rm.mynotes.utils.dto.payloads.ResponseDTO; +import com.rm.mynotes.utils.dto.requests.ReminderDTO; +import com.rm.mynotes.utils.functions.CommonFunctions; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Service; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import java.net.URI; +import java.util.List; +import java.util.Objects; + +@Service +@RequiredArgsConstructor +public class ReminderServiceImplementation implements ReminderService { + @Autowired + private UserRepository userRepository; + + @Autowired + private NoteRepository noteRepository; + + @Autowired + private ReminderRepository reminderRepository; + + @Autowired + private CommonFunctions commonFunctions; + + @Override + public ResponseEntity createReminder(Authentication authentication, ReminderDTO reqReminder, Long noteId) { + UserEntity user = commonFunctions.getCurrentUser(authentication); + Note note = user.getNotes().stream().filter(userNote -> Objects.equals(userNote.getId(), noteId)) + .findFirst().orElseThrow(() -> new BadCredentialsException("A anotação informada não existe.")); + + Reminder reminder = Reminder.builder() + .reminderDate(reqReminder.getReminderDate()) + .createdAt(CommonFunctions.getCurrentDatetime()) + .lastUpdate(CommonFunctions.getCurrentDatetime()) + .status(StatusTypes.PENDING) + .build(); + + List reminders = note.getReminders(); + reminders.add(reminderRepository.save(reminder)); + + note.setReminders(reminders); + noteRepository.save(note); + + URI uri = URI.create(ServletUriComponentsBuilder.fromCurrentContextPath().path(RoutePaths.REMINDER).toUriString()); + + return ResponseEntity.created(uri).body(reminder); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleException(Exception exception) { + return CommonFunctions.errorHandling(exception); + } +} diff --git a/src/main/java/com/rm/mynotes/service/mold/ReminderService.java b/src/main/java/com/rm/mynotes/service/mold/ReminderService.java new file mode 100644 index 0000000..ca14e27 --- /dev/null +++ b/src/main/java/com/rm/mynotes/service/mold/ReminderService.java @@ -0,0 +1,10 @@ +package com.rm.mynotes.service.mold; + +import com.rm.mynotes.model.Reminder; +import com.rm.mynotes.utils.dto.requests.ReminderDTO; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; + +public interface ReminderService { + ResponseEntity createReminder(Authentication authentication, ReminderDTO reqReminder, Long noteId); +} diff --git a/src/main/java/com/rm/mynotes/utils/config/KafkaConsumerConfig.java b/src/main/java/com/rm/mynotes/utils/config/KafkaConsumerConfig.java new file mode 100644 index 0000000..a334301 --- /dev/null +++ b/src/main/java/com/rm/mynotes/utils/config/KafkaConsumerConfig.java @@ -0,0 +1,27 @@ +package com.rm.mynotes.utils.config; + +import com.rm.mynotes.model.Notification; +import com.rm.mynotes.model.Reminder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.annotation.EnableKafka; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.messaging.simp.SimpMessagingTemplate; + +@Configuration +@EnableKafka +public class KafkaConsumerConfig { + @Autowired + private SimpMessagingTemplate messagingTemplate; + + @KafkaListener(topics = "remindersTopic", groupId = "group1") + public void listen(String message) { + Reminder reminder = parseReminder(message); + Notification notification = new Notification(); + messagingTemplate.convertAndSend("/app/topic/notifications", notification); + } + + private Reminder parseReminder(String message) { + return new Reminder(); + } +} diff --git a/src/main/java/com/rm/mynotes/utils/config/KafkaProducerConfig.java b/src/main/java/com/rm/mynotes/utils/config/KafkaProducerConfig.java new file mode 100644 index 0000000..67d6e59 --- /dev/null +++ b/src/main/java/com/rm/mynotes/utils/config/KafkaProducerConfig.java @@ -0,0 +1,32 @@ +package com.rm.mynotes.utils.config; + +import org.apache.kafka.common.serialization.StringSerializer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.core.DefaultKafkaProducerFactory; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.core.ProducerFactory; + +import java.util.HashMap; +import java.util.Map; + +@Configuration +public class KafkaProducerConfig { + @Value("${spring.kafka.bootstrap-servers}") + private String bootstrapServers; + + @Bean + public ProducerFactory producerFactory() { + Map configProps = new HashMap<>(); + configProps.put(org.apache.kafka.clients.producer.ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + configProps.put(org.apache.kafka.clients.producer.ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + configProps.put(org.apache.kafka.clients.producer.ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + return new DefaultKafkaProducerFactory<>(configProps); + } + + @Bean + public KafkaTemplate kafkaTemplate() { + return new KafkaTemplate<>(producerFactory()); + } +} diff --git a/src/main/java/com/rm/mynotes/utils/config/WebSocketConfig.java b/src/main/java/com/rm/mynotes/utils/config/WebSocketConfig.java new file mode 100644 index 0000000..cbbb086 --- /dev/null +++ b/src/main/java/com/rm/mynotes/utils/config/WebSocketConfig.java @@ -0,0 +1,22 @@ +package com.rm.mynotes.utils.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; + +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + @Override + public void configureMessageBroker(MessageBrokerRegistry config) { + config.enableSimpleBroker("/topic"); + config.setApplicationDestinationPrefixes("/app"); + } + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/websocket").withSockJS(); + } +} diff --git a/src/main/java/com/rm/mynotes/utils/constants/RoutePaths.java b/src/main/java/com/rm/mynotes/utils/constants/RoutePaths.java index d03e7bc..788d6e3 100644 --- a/src/main/java/com/rm/mynotes/utils/constants/RoutePaths.java +++ b/src/main/java/com/rm/mynotes/utils/constants/RoutePaths.java @@ -1,6 +1,7 @@ package com.rm.mynotes.utils.constants; public class RoutePaths { + public static final String REMINDER = "/app/reminder"; public static final String LOGIN = "/api/auth/login"; public static final String SIGNUP = "/api/auth/signup"; public static final String GET_NOTE = "/api/note/{id}"; diff --git a/src/main/java/com/rm/mynotes/utils/constants/StatusTypes.java b/src/main/java/com/rm/mynotes/utils/constants/StatusTypes.java new file mode 100644 index 0000000..de0771c --- /dev/null +++ b/src/main/java/com/rm/mynotes/utils/constants/StatusTypes.java @@ -0,0 +1,5 @@ +package com.rm.mynotes.utils.constants; + +public enum StatusTypes { + PENDING, LATE, DONE, DONELATE +} diff --git a/src/main/java/com/rm/mynotes/utils/dto/requests/ReminderDTO.java b/src/main/java/com/rm/mynotes/utils/dto/requests/ReminderDTO.java new file mode 100644 index 0000000..9810c90 --- /dev/null +++ b/src/main/java/com/rm/mynotes/utils/dto/requests/ReminderDTO.java @@ -0,0 +1,16 @@ +package com.rm.mynotes.utils.dto.requests; + +import com.rm.mynotes.utils.constants.StatusTypes; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.time.OffsetDateTime; + +@Data +public class ReminderDTO { + @NotNull(message = "O título não pode está vázio") + private String title; + + @NotNull(message = "Deve haver ao menos uma data") + private OffsetDateTime reminderDate; +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a466188..b3af907 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -12,6 +12,8 @@ spring: driver-class-name: com.mysql.cj.jdbc.Driver username: ${DB_USERNAME} password: ${DB_PASSWORD} + kafka: + bootstrap-servers: ${KAFKA_SERVERS} jpa: hibernate: ddl-auto: create-drop