commit 0fa2238557e67b96581314e503372c878a9d98f3 Author: Patrick Haßel Date: Tue Apr 15 14:34:57 2025 +0200 PatrixCrud, PatrixMqtt, PatrixDemo diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ff6309 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/PatrixCrud/pom.xml b/PatrixCrud/pom.xml new file mode 100644 index 0000000..acc4310 --- /dev/null +++ b/PatrixCrud/pom.xml @@ -0,0 +1,42 @@ + + + 4.0.0 + + PatrixCrud + + + de.ph87.patrix + PatrixParent + 1.0-SNAPSHOT + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-websocket + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.projectlombok + lombok + + + com.h2database + h2 + + + org.postgresql + postgresql + + + + \ No newline at end of file diff --git a/PatrixCrud/src/main/java/de/ph87/patrix/crud/CrudAction.java b/PatrixCrud/src/main/java/de/ph87/patrix/crud/CrudAction.java new file mode 100644 index 0000000..f18930e --- /dev/null +++ b/PatrixCrud/src/main/java/de/ph87/patrix/crud/CrudAction.java @@ -0,0 +1,5 @@ +package de.ph87.patrix.crud; + +public enum CrudAction { + CREATED, MODIFIED, DELETED +} diff --git a/PatrixCrud/src/main/java/de/ph87/patrix/crud/CrudController.java b/PatrixCrud/src/main/java/de/ph87/patrix/crud/CrudController.java new file mode 100644 index 0000000..46aeb37 --- /dev/null +++ b/PatrixCrud/src/main/java/de/ph87/patrix/crud/CrudController.java @@ -0,0 +1,47 @@ +package de.ph87.patrix.crud; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RequiredArgsConstructor +public abstract class CrudController, Service extends CrudService> { + + protected final Service service; + + protected final Repository repository; + + @NonNull + @GetMapping("{id}") + public Dto byId(@PathVariable @NonNull final ID id) { + return repository.getDtoById(id); + } + + @NonNull + @PostMapping("list") + public List list(@RequestBody @NonNull final CrudFilter filter) { + return repository.findAllDto(filter.getSpecification(), filter.getSort()); + } + + @NonNull + @PostMapping("page") + public Page page(@RequestBody @NonNull final CrudFilter filter) { + return repository.findAllDto(filter.getSpecification(), filter.getPageable()); + } + + @NonNull + @PostMapping("create") + public Dto create(@RequestBody @NonNull final Create create) { + return service.create(create); + } + + @NonNull + @DeleteMapping("{id}") + public Dto delete(@PathVariable @NonNull final ID id) { + return service.delete(id, null); + } + +} diff --git a/PatrixCrud/src/main/java/de/ph87/patrix/crud/CrudEvent.java b/PatrixCrud/src/main/java/de/ph87/patrix/crud/CrudEvent.java new file mode 100644 index 0000000..4b828d1 --- /dev/null +++ b/PatrixCrud/src/main/java/de/ph87/patrix/crud/CrudEvent.java @@ -0,0 +1,23 @@ +package de.ph87.patrix.crud; + +import lombok.Data; +import lombok.NonNull; + +import java.time.ZonedDateTime; + +@Data +public class CrudEvent { + + @NonNull + public final ZonedDateTime timestamp = ZonedDateTime.now(); + + @NonNull + public final String topic; + + @NonNull + public final T payload; + + @NonNull + public final CrudAction action; + +} diff --git a/PatrixCrud/src/main/java/de/ph87/patrix/crud/CrudFilter.java b/PatrixCrud/src/main/java/de/ph87/patrix/crud/CrudFilter.java new file mode 100644 index 0000000..2df22a7 --- /dev/null +++ b/PatrixCrud/src/main/java/de/ph87/patrix/crud/CrudFilter.java @@ -0,0 +1,50 @@ +package de.ph87.patrix.crud; + +import lombok.*; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; + +import java.util.List; + +@Getter +@ToString +@AllArgsConstructor +public class CrudFilter { + + public final int page; + + public final int size; + + public final List orders; + + @NonNull + public Sort getSort() { + return Sort.by(orders.stream().map(Order::toSortOrder).toList()); + } + + @Data + public static class Order { + + public final String property; + + public final Sort.Direction direction; + + public Sort.Order toSortOrder() { + return new Sort.Order(direction, property); + } + + } + + @NonNull + public Pageable getPageable() { + return PageRequest.of(page, size, getSort()); + } + + @NonNull + public Specification getSpecification() { + return Specification.where(null); + } + +} diff --git a/PatrixCrud/src/main/java/de/ph87/patrix/crud/CrudRepository.java b/PatrixCrud/src/main/java/de/ph87/patrix/crud/CrudRepository.java new file mode 100644 index 0000000..583d642 --- /dev/null +++ b/PatrixCrud/src/main/java/de/ph87/patrix/crud/CrudRepository.java @@ -0,0 +1,36 @@ +package de.ph87.patrix.crud; + +import jakarta.annotation.Nullable; +import lombok.NonNull; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.repository.ListCrudRepository; +import org.springframework.data.repository.NoRepositoryBean; + +import java.util.List; + +@NoRepositoryBean +public interface CrudRepository extends ListCrudRepository, JpaSpecificationExecutor { + + @NonNull + default List findAllDto(@Nullable final Specification specification, @NonNull final Sort sort) { + return findAll(specification, sort).stream().map(this::toDto).toList(); + } + + @NonNull + default Page findAllDto(@NonNull final Specification specification, @NonNull final Pageable pageable) { + return findAll(specification, pageable).map(this::toDto); + } + + @NonNull + default Dto getDtoById(@NonNull ID id) { + return findById(id).map(this::toDto).orElseThrow(); + } + + @NonNull + Dto toDto(@NonNull final Entity entity); + +} diff --git a/PatrixCrud/src/main/java/de/ph87/patrix/crud/CrudService.java b/PatrixCrud/src/main/java/de/ph87/patrix/crud/CrudService.java new file mode 100644 index 0000000..c3c2ba7 --- /dev/null +++ b/PatrixCrud/src/main/java/de/ph87/patrix/crud/CrudService.java @@ -0,0 +1,57 @@ +package de.ph87.patrix.crud; + +import jakarta.annotation.Nullable; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.transaction.annotation.Transactional; + +import java.util.function.Consumer; + +@Slf4j +@RequiredArgsConstructor +public abstract class CrudService> { + + protected final ApplicationEventPublisher applicationEventPublisher; + + protected final Repository repository; + + @NonNull + @Transactional + public Dto create(@NonNull final Create create) { + return publish(repository.save(toEntity(create)), CrudAction.CREATED); + } + + @NonNull + @Transactional + public Dto modify(@NonNull final ID id, @NonNull final Consumer modifier) { + final Entity entity = repository.findById(id).orElseThrow(); + modifier.accept(entity); + return publish(entity, CrudAction.MODIFIED); + } + + @NonNull + @Transactional + public Dto delete(@NonNull final ID id, @Nullable final Consumer preDelete) { + final Entity entity = repository.findById(id).orElseThrow(); + final Dto dto = publish(entity, CrudAction.DELETED); + if (preDelete != null) { + preDelete.accept(entity); + } + repository.delete(entity); + return dto; + } + + @NonNull + protected Dto publish(@NonNull final Entity entity, @NonNull final CrudAction action) { + final Dto dto = repository.toDto(entity); + log.info("{} {}: {}", entity.getClass().getSimpleName(), action, dto); + applicationEventPublisher.publishEvent(new CrudEvent<>(entity.getClass().getSimpleName(), dto, action)); + return dto; + } + + @NonNull + protected abstract Entity toEntity(@NonNull final Create create); + +} diff --git a/PatrixCrud/src/main/java/de/ph87/patrix/crud/CrudWebConfig.java b/PatrixCrud/src/main/java/de/ph87/patrix/crud/CrudWebConfig.java new file mode 100644 index 0000000..a3a1951 --- /dev/null +++ b/PatrixCrud/src/main/java/de/ph87/patrix/crud/CrudWebConfig.java @@ -0,0 +1,16 @@ +package de.ph87.patrix.crud; + +import lombok.NonNull; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class CrudWebConfig implements WebMvcConfigurer { + + @Override + public void addCorsMappings(@NonNull final CorsRegistry registry) { + registry.addMapping("/**").allowedOrigins("*").allowedMethods("*"); + } + +} diff --git a/PatrixCrud/src/main/java/de/ph87/patrix/crud/CrudWebsocketConfig.java b/PatrixCrud/src/main/java/de/ph87/patrix/crud/CrudWebsocketConfig.java new file mode 100644 index 0000000..598d72c --- /dev/null +++ b/PatrixCrud/src/main/java/de/ph87/patrix/crud/CrudWebsocketConfig.java @@ -0,0 +1,32 @@ +package de.ph87.patrix.crud; + +import lombok.NonNull; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +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 CrudWebsocketConfig implements WebSocketMessageBrokerConfigurer { + + @Override + public void registerStompEndpoints(@NonNull final StompEndpointRegistry registry) { + registry.addEndpoint("/websocket").setAllowedOrigins("*"); + } + + @Override + public void configureMessageBroker(@NonNull final MessageBrokerRegistry config) { + config.enableSimpleBroker().setHeartbeatValue(new long[]{2000, 2000}).setTaskScheduler(heartBeatScheduler()); + } + + @Bean + public TaskScheduler heartBeatScheduler() { + return new ThreadPoolTaskScheduler(); + } + +} diff --git a/PatrixCrud/src/main/java/de/ph87/patrix/crud/CrudWebsocketService.java b/PatrixCrud/src/main/java/de/ph87/patrix/crud/CrudWebsocketService.java new file mode 100644 index 0000000..350be9a --- /dev/null +++ b/PatrixCrud/src/main/java/de/ph87/patrix/crud/CrudWebsocketService.java @@ -0,0 +1,22 @@ +package de.ph87.patrix.crud; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.lang.NonNull; +import org.springframework.messaging.simp.SimpMessageSendingOperations; +import org.springframework.stereotype.Service; +import org.springframework.transaction.event.TransactionalEventListener; + +@Slf4j +@Service +@RequiredArgsConstructor +public class CrudWebsocketService { + + private final SimpMessageSendingOperations simpMessageSendingOperations; + + @TransactionalEventListener(CrudEvent.class) + public void send(@NonNull final CrudEvent event) { + simpMessageSendingOperations.convertAndSend(event.getTopic(), event); + } + +} diff --git a/PatrixCrud/src/main/resources/application.properties b/PatrixCrud/src/main/resources/application.properties new file mode 100644 index 0000000..f9fffb6 --- /dev/null +++ b/PatrixCrud/src/main/resources/application.properties @@ -0,0 +1,10 @@ +logging.level.root=WARN +logging.level.de.ph87=INFO +#- +spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyComponentPathImpl +spring.jpa.hibernate.ddl-auto=update +spring.jpa.open-in-view=false +#- +spring.jackson.serialization.indent_output=true +#- +spring.main.banner-mode=off diff --git a/PatrixDemo/pom.xml b/PatrixDemo/pom.xml new file mode 100644 index 0000000..5d98a04 --- /dev/null +++ b/PatrixDemo/pom.xml @@ -0,0 +1,32 @@ + + + 4.0.0 + + PatrixDemo + + + de.ph87.patrix + PatrixParent + 1.0-SNAPSHOT + + + + + de.ph87.patrix + PatrixCrud + 1.0-SNAPSHOT + + + de.ph87.patrix + PatrixMqtt + 1.0-SNAPSHOT + + + org.springframework.security + spring-security-core + + + + \ No newline at end of file diff --git a/PatrixDemo/src/main/java/de/ph87/patrix/demo/Backend.java b/PatrixDemo/src/main/java/de/ph87/patrix/demo/Backend.java new file mode 100644 index 0000000..4c0e448 --- /dev/null +++ b/PatrixDemo/src/main/java/de/ph87/patrix/demo/Backend.java @@ -0,0 +1,23 @@ +package de.ph87.patrix.demo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@SpringBootApplication +@ComponentScan(basePackages = {"de.ph87.patrix"}) +public class Backend { + + public static void main(String[] args) { + SpringApplication.run(Backend.class, args); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + +} \ No newline at end of file diff --git a/PatrixDemo/src/main/java/de/ph87/patrix/demo/DemoService.java b/PatrixDemo/src/main/java/de/ph87/patrix/demo/DemoService.java new file mode 100644 index 0000000..f40ca14 --- /dev/null +++ b/PatrixDemo/src/main/java/de/ph87/patrix/demo/DemoService.java @@ -0,0 +1,25 @@ +package de.ph87.patrix.demo; + +import de.ph87.patrix.demo.user.UserController; +import de.ph87.patrix.demo.user.UserCreate; +import de.ph87.patrix.demo.user.UserDto; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.event.ApplicationStartedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class DemoService { + + private final UserController userController; + + @EventListener(ApplicationStartedEvent.class) + public void insertDemoData() { + final UserDto patrick = userController.create(new UserCreate(true, "patrick", "1234", "Patrick", "Haßel")); + final UserDto katrin = userController.create(new UserCreate(true, "katrin", null, "Katrin", "Haßel")); + } + +} diff --git a/PatrixDemo/src/main/java/de/ph87/patrix/demo/user/DuplicateUser.java b/PatrixDemo/src/main/java/de/ph87/patrix/demo/user/DuplicateUser.java new file mode 100644 index 0000000..1862109 --- /dev/null +++ b/PatrixDemo/src/main/java/de/ph87/patrix/demo/user/DuplicateUser.java @@ -0,0 +1,13 @@ +package de.ph87.patrix.demo.user; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.BAD_REQUEST) +public class DuplicateUser extends RuntimeException { + + public DuplicateUser() { + super("Duplicate user"); + } + +} diff --git a/PatrixDemo/src/main/java/de/ph87/patrix/demo/user/PasswordNotAllowed.java b/PatrixDemo/src/main/java/de/ph87/patrix/demo/user/PasswordNotAllowed.java new file mode 100644 index 0000000..0c21ca4 --- /dev/null +++ b/PatrixDemo/src/main/java/de/ph87/patrix/demo/user/PasswordNotAllowed.java @@ -0,0 +1,13 @@ +package de.ph87.patrix.demo.user; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.BAD_REQUEST) +public class PasswordNotAllowed extends RuntimeException { + + public PasswordNotAllowed(final String reason) { + super("Password not allowed: " + reason); + } + +} diff --git a/PatrixDemo/src/main/java/de/ph87/patrix/demo/user/User.java b/PatrixDemo/src/main/java/de/ph87/patrix/demo/user/User.java new file mode 100644 index 0000000..78ef766 --- /dev/null +++ b/PatrixDemo/src/main/java/de/ph87/patrix/demo/user/User.java @@ -0,0 +1,71 @@ +package de.ph87.patrix.demo.user; + +import jakarta.annotation.Nullable; +import jakarta.persistence.*; +import lombok.*; +import org.springframework.security.crypto.password.PasswordEncoder; + +import java.time.ZonedDateTime; + +@Entity +@Getter +@ToString +@NoArgsConstructor +@Table(name = "`user`") +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Version + private long version; + + @NonNull + @Column(nullable = false, updatable = false) + private ZonedDateTime created = ZonedDateTime.now(); + + @Column(nullable = false) + private boolean enabled; + + @Setter + @NonNull + @Column(nullable = false, unique = true) + private String name; + + @Setter + @NonNull + @Column(nullable = false) + private String password; + + @Setter + @NonNull + @Column(nullable = false) + private String firstname = ""; + + @Setter + @NonNull + @Column(nullable = false) + private String lastname = ""; + + public User(@NonNull final UserCreate create, @NonNull final PasswordEncoder passwordEncoder) { + this.enabled = create.enabled; + this.name = create.name; + this.setPassword(create.password, passwordEncoder); + this.firstname = create.firstname; + this.lastname = create.lastname; + } + + @SneakyThrows + public void setPassword(@Nullable final String password, @NonNull final PasswordEncoder passwordEncoder) { + if (password == null || password.isEmpty()) { + this.password = ""; + return; + } + if (password.length() < 4) { + throw new PasswordNotAllowed("Too short"); + } + this.password = passwordEncoder.encode(password); + } + +} diff --git a/PatrixDemo/src/main/java/de/ph87/patrix/demo/user/UserController.java b/PatrixDemo/src/main/java/de/ph87/patrix/demo/user/UserController.java new file mode 100644 index 0000000..b829251 --- /dev/null +++ b/PatrixDemo/src/main/java/de/ph87/patrix/demo/user/UserController.java @@ -0,0 +1,48 @@ +package de.ph87.patrix.demo.user; + +import de.ph87.patrix.crud.CrudController; +import jakarta.annotation.Nullable; +import lombok.NonNull; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("User") +public class UserController extends CrudController { + + private final PasswordEncoder passwordEncoder; + + public UserController(final UserService service, final UserRepository repository, final PasswordEncoder passwordEncoder) { + super(service, repository); + this.passwordEncoder = passwordEncoder; + } + + @PostMapping("{id}/name") + public UserDto name(@PathVariable final long id, @RequestBody @NonNull final String name) { + return service.modify(id, user -> { + if (user.getName().equals(name)) { + return; + } + if (repository.existsByName(name)) { + throw new DuplicateUser(); + } + user.setName(name); + }); + } + + @PostMapping("{id}/password") + public UserDto password(@PathVariable final long id, @RequestBody(required = false) @Nullable final String password) { + return service.modify(id, user -> user.setPassword(password, passwordEncoder)); + } + + @PostMapping("{id}/firstname") + public UserDto firstname(@PathVariable final long id, @RequestBody @NonNull final String firstname) { + return service.modify(id, user -> user.setFirstname(firstname)); + } + + @PostMapping("{id}/lastname") + public UserDto lastname(@PathVariable final long id, @RequestBody @NonNull final String lastname) { + return service.modify(id, user -> user.setLastname(lastname)); + } + +} diff --git a/PatrixDemo/src/main/java/de/ph87/patrix/demo/user/UserCreate.java b/PatrixDemo/src/main/java/de/ph87/patrix/demo/user/UserCreate.java new file mode 100644 index 0000000..063e626 --- /dev/null +++ b/PatrixDemo/src/main/java/de/ph87/patrix/demo/user/UserCreate.java @@ -0,0 +1,28 @@ +package de.ph87.patrix.demo.user; + +import jakarta.annotation.Nullable; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NonNull; +import lombok.ToString; + +@Getter +@ToString +@AllArgsConstructor +public class UserCreate { + + public final boolean enabled; + + @NonNull + public final String name; + + @Nullable + public final String password; + + @NonNull + public final String firstname; + + @NonNull + public final String lastname; + +} diff --git a/PatrixDemo/src/main/java/de/ph87/patrix/demo/user/UserDto.java b/PatrixDemo/src/main/java/de/ph87/patrix/demo/user/UserDto.java new file mode 100644 index 0000000..c1d94cb --- /dev/null +++ b/PatrixDemo/src/main/java/de/ph87/patrix/demo/user/UserDto.java @@ -0,0 +1,45 @@ +package de.ph87.patrix.demo.user; + +import lombok.Getter; +import lombok.NonNull; +import lombok.ToString; + +import java.time.ZonedDateTime; + +@Getter +@ToString +public class UserDto { + + public final long id; + + public final long version; + + @NonNull + @ToString.Exclude + public final ZonedDateTime created; + + public final boolean enabled; + + @NonNull + public final String name; + + public final boolean password; + + @NonNull + public final String firstname; + + @NonNull + public final String lastname; + + public UserDto(@NonNull final User user) { + this.id = user.getId(); + this.version = user.getVersion(); + this.created = user.getCreated(); + this.enabled = user.isEnabled(); + this.name = user.getName(); + this.password = !user.getPassword().isEmpty(); + this.firstname = user.getFirstname(); + this.lastname = user.getLastname(); + } + +} diff --git a/PatrixDemo/src/main/java/de/ph87/patrix/demo/user/UserFilter.java b/PatrixDemo/src/main/java/de/ph87/patrix/demo/user/UserFilter.java new file mode 100644 index 0000000..8c1fb13 --- /dev/null +++ b/PatrixDemo/src/main/java/de/ph87/patrix/demo/user/UserFilter.java @@ -0,0 +1,17 @@ +package de.ph87.patrix.demo.user; + +import de.ph87.patrix.crud.CrudFilter; +import lombok.Getter; +import lombok.ToString; + +import java.util.List; + +@Getter +@ToString +public class UserFilter extends CrudFilter { + + public UserFilter(final int page, final int size, final List orders) { + super(page, size, orders); + } + +} diff --git a/PatrixDemo/src/main/java/de/ph87/patrix/demo/user/UserRepository.java b/PatrixDemo/src/main/java/de/ph87/patrix/demo/user/UserRepository.java new file mode 100644 index 0000000..5088eb2 --- /dev/null +++ b/PatrixDemo/src/main/java/de/ph87/patrix/demo/user/UserRepository.java @@ -0,0 +1,16 @@ +package de.ph87.patrix.demo.user; + +import de.ph87.patrix.crud.CrudRepository; +import lombok.NonNull; + +public interface UserRepository extends CrudRepository { + + boolean existsByName(@NonNull String name); + + @NonNull + @Override + default UserDto toDto(@NonNull final User user) { + return new UserDto(user); + } + +} diff --git a/PatrixDemo/src/main/java/de/ph87/patrix/demo/user/UserService.java b/PatrixDemo/src/main/java/de/ph87/patrix/demo/user/UserService.java new file mode 100644 index 0000000..faca7a0 --- /dev/null +++ b/PatrixDemo/src/main/java/de/ph87/patrix/demo/user/UserService.java @@ -0,0 +1,36 @@ +package de.ph87.patrix.demo.user; + +import de.ph87.patrix.crud.CrudService; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class UserService extends CrudService { + + private final PasswordEncoder passwordEncoder; + + public UserService(final UserRepository repository, final ApplicationEventPublisher applicationEventPublisher, final PasswordEncoder passwordEncoder) { + super(applicationEventPublisher, repository); + this.passwordEncoder = passwordEncoder; + } + + @NonNull + @Override + public UserDto create(@NonNull final UserCreate userCreate) { + if (repository.existsByName(userCreate.name)) { + throw new DuplicateUser(); + } + return super.create(userCreate); + } + + @NonNull + @Override + protected User toEntity(@NonNull final UserCreate create) { + return new User(create, passwordEncoder); + } + +} diff --git a/PatrixDemo/src/main/resources/application.properties b/PatrixDemo/src/main/resources/application.properties new file mode 100644 index 0000000..f9fffb6 --- /dev/null +++ b/PatrixDemo/src/main/resources/application.properties @@ -0,0 +1,10 @@ +logging.level.root=WARN +logging.level.de.ph87=INFO +#- +spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyComponentPathImpl +spring.jpa.hibernate.ddl-auto=update +spring.jpa.open-in-view=false +#- +spring.jackson.serialization.indent_output=true +#- +spring.main.banner-mode=off diff --git a/PatrixMqtt/pom.xml b/PatrixMqtt/pom.xml new file mode 100644 index 0000000..8d2cc6f --- /dev/null +++ b/PatrixMqtt/pom.xml @@ -0,0 +1,31 @@ + + + 4.0.0 + + PatrixMqtt + + + de.ph87.patrix + PatrixParent + 1.0-SNAPSHOT + + + + + org.springframework.boot + spring-boot-starter + + + org.projectlombok + lombok + + + com.hivemq + hivemq-mqtt-client + 1.3.5 + + + + \ No newline at end of file diff --git a/PatrixMqtt/src/main/java/de/ph87/patrix/mqtt/MqttConfig.java b/PatrixMqtt/src/main/java/de/ph87/patrix/mqtt/MqttConfig.java new file mode 100644 index 0000000..efd8e10 --- /dev/null +++ b/PatrixMqtt/src/main/java/de/ph87/patrix/mqtt/MqttConfig.java @@ -0,0 +1,32 @@ +package de.ph87.patrix.mqtt; + +import lombok.Data; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.UUID; + +@Data +@Slf4j +@Component +@ConfigurationProperties(prefix = "de.ph87.patrix.mqtt") +public class MqttConfig { + + @NonNull + private String host = ""; + + private int port = 1883; + + @NonNull + private String clientId = "TEMPORARY-" + UUID.randomUUID(); + + private boolean cleanSession = true; + + @NonNull + public String getUrl() { + return "tcp://" + host + ":" + port; + } + +} diff --git a/PatrixMqtt/src/main/java/de/ph87/patrix/mqtt/MqttEvent.java b/PatrixMqtt/src/main/java/de/ph87/patrix/mqtt/MqttEvent.java new file mode 100644 index 0000000..58a2f09 --- /dev/null +++ b/PatrixMqtt/src/main/java/de/ph87/patrix/mqtt/MqttEvent.java @@ -0,0 +1,30 @@ +package de.ph87.patrix.mqtt; + +import com.hivemq.client.mqtt.mqtt3.message.publish.Mqtt3Publish; +import lombok.Getter; +import lombok.NonNull; +import lombok.ToString; + +import java.nio.charset.StandardCharsets; +import java.time.ZonedDateTime; + +@Getter +@ToString +public class MqttEvent { + + public final ZonedDateTime received = ZonedDateTime.now(); + + public final String topic; + + @ToString.Exclude + public final String payload; + + public final String payloadLoggable; + + public MqttEvent(@NonNull final Mqtt3Publish publish) { + this.topic = publish.getTopic().toString(); + this.payload = new String(publish.getPayloadAsBytes(), StandardCharsets.UTF_8); + this.payloadLoggable = this.payload.replace("\n", "\\n").replace("\r", "\\r"); + } + +} diff --git a/PatrixMqtt/src/main/java/de/ph87/patrix/mqtt/MqttService.java b/PatrixMqtt/src/main/java/de/ph87/patrix/mqtt/MqttService.java new file mode 100644 index 0000000..88e962f --- /dev/null +++ b/PatrixMqtt/src/main/java/de/ph87/patrix/mqtt/MqttService.java @@ -0,0 +1,62 @@ +package de.ph87.patrix.mqtt; + +import com.hivemq.client.mqtt.MqttClient; +import com.hivemq.client.mqtt.datatypes.MqttQos; +import com.hivemq.client.mqtt.mqtt3.Mqtt3AsyncClient; +import com.hivemq.client.mqtt.mqtt3.message.publish.Mqtt3Publish; +import jakarta.annotation.PreDestroy; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class MqttService { + + private final ApplicationEventPublisher applicationEventPublisher; + + private final MqttConfig config; + + private Mqtt3AsyncClient client; + + @EventListener(ApplicationReadyEvent.class) + public void connect() { + if (client != null) { + throw new RuntimeException(); + } + client = MqttClient.builder() + .useMqttVersion3() + .identifier(config.getClientId()) + .serverHost(config.getHost()) + .serverPort(config.getPort()) + .buildAsync(); + client.connectWith().cleanSession(config.isCleanSession()).send(); + client.subscribeWith().topicFilter("#").qos(MqttQos.AT_LEAST_ONCE).callback(this::receive).send(); + log.info("Connected: {} as {} {}", config.getUrl(), config.getClientId(), config.isCleanSession() ? "[CLEAN SESSION]" : "[PERSISTENT SESSION]"); + } + + @PreDestroy + public void shutdown() { + if (client != null) { + log.info("Disconnecting..."); + client.disconnect(); + client = null; + } + } + + public void receive(@NonNull final Mqtt3Publish publish) { + try { + final MqttEvent event = new MqttEvent(publish); + log.debug("Message received: topic={}, payload={}", event.getTopic(), event.getPayloadLoggable()); + applicationEventPublisher.publishEvent(event); + } catch (Exception e) { + log.error("Unexpected Exception publishing MqttEvent.", e); + } + } + +} diff --git a/application.properties b/application.properties new file mode 100644 index 0000000..19661e3 --- /dev/null +++ b/application.properties @@ -0,0 +1,10 @@ +logging.level.de.ph87=DEBUG +#- +spring.datasource.url=jdbc:h2:./database;AUTO_SERVER=TRUE +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password=password +#- +spring.jpa.hibernate.ddl-auto=create +#- +de.ph87.patrix.mqtt.host=10.0.0.50 \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..3b76803 --- /dev/null +++ b/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + + + 21 + 21 + 21 + UTF-8 + + + de.ph87.patrix + PatrixParent + 1.0-SNAPSHOT + pom + + + PatrixCrud + PatrixDemo + PatrixMqtt + + + + org.springframework.boot + spring-boot-starter-parent + 3.4.4 + + + + + + de.ph87.patrix + PatrixCrud + 1.0-SNAPSHOT + + + de.ph87.patrix + PatrixMqtt + 1.0-SNAPSHOT + + + de.ph87.patrix + PatrixDemo + 1.0-SNAPSHOT + + + + + \ No newline at end of file