diff --git a/pom.xml b/pom.xml index 405168e..ba7f1d3 100644 --- a/pom.xml +++ b/pom.xml @@ -96,6 +96,11 @@ spring-boot-starter-webmvc-test test + + cn.hutool + hutool-core + 5.8.43 + diff --git a/src/main/java/dev/mednikov/social/connections/config/JdbcTemplateConfig.java b/src/main/java/dev/mednikov/social/connections/config/JdbcTemplateConfig.java new file mode 100644 index 0000000..5ae72d0 --- /dev/null +++ b/src/main/java/dev/mednikov/social/connections/config/JdbcTemplateConfig.java @@ -0,0 +1,17 @@ +package dev.mednikov.social.connections.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; + +import javax.sql.DataSource; + +@Configuration +public class JdbcTemplateConfig { + + @Bean + public NamedParameterJdbcTemplate namedParameterJdbcTemplate(DataSource dataSource) { + return new NamedParameterJdbcTemplate(dataSource); + } + +} diff --git a/src/main/java/dev/mednikov/social/connections/config/ObjectMapperConfig.java b/src/main/java/dev/mednikov/social/connections/config/ObjectMapperConfig.java index 37d5b58..aaa376e 100644 --- a/src/main/java/dev/mednikov/social/connections/config/ObjectMapperConfig.java +++ b/src/main/java/dev/mednikov/social/connections/config/ObjectMapperConfig.java @@ -1,5 +1,7 @@ package dev.mednikov.social.connections.config; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -10,6 +12,7 @@ public class ObjectMapperConfig { @Bean public ObjectMapper objectMapper() { ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); return objectMapper; } } diff --git a/src/main/java/dev/mednikov/social/connections/messaging/UserCreatedListener.java b/src/main/java/dev/mednikov/social/connections/messaging/UserCreatedListener.java new file mode 100644 index 0000000..176819d --- /dev/null +++ b/src/main/java/dev/mednikov/social/connections/messaging/UserCreatedListener.java @@ -0,0 +1,63 @@ +package dev.mednikov.social.connections.messaging; + +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.mednikov.social.connections.models.User; +import jakarta.transaction.Transactional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.stereotype.Component; + +@Component +public class UserCreatedListener { + + private final static UserParamsMapper userParamsMapper = new UserParamsMapper(); + private final static Logger logger = LoggerFactory.getLogger(UserCreatedListener.class); + + private final NamedParameterJdbcTemplate jdbcTemplate; + private final ObjectMapper objectMapper; + + public UserCreatedListener(NamedParameterJdbcTemplate jdbcTemplate, ObjectMapper objectMapper) { + this.jdbcTemplate = jdbcTemplate; + this.objectMapper = objectMapper; + } + + @Transactional + @RabbitListener(queues = {"social_users_created"}) + public void onEvent (String payload) { + logger.info("User created" + payload); + try { + UserPayload user = this.objectMapper.readValue(payload, UserPayload.class); + String query = """ + INSERT INTO connections_user ( + id, + first_name, + last_name, + email, + is_active, + is_onboarded, + is_email_verified, + avatar_url, + headline, + version + ) VALUES ( + :id, + :first_name, + :last_name, + :email, + :is_active, + :is_onboarded, + :is_email_verified, + :avatar_url, + :headline, + :version) ON CONFLICT (id) DO NOTHING + """; + MapSqlParameterSource sqlParameterSource = userParamsMapper.apply(user); + jdbcTemplate.update(query, sqlParameterSource); + } catch (Exception e) { + logger.error(e.getMessage()); + } + } +} diff --git a/src/main/java/dev/mednikov/social/connections/messaging/UserParamsMapper.java b/src/main/java/dev/mednikov/social/connections/messaging/UserParamsMapper.java new file mode 100644 index 0000000..45e7188 --- /dev/null +++ b/src/main/java/dev/mednikov/social/connections/messaging/UserParamsMapper.java @@ -0,0 +1,24 @@ +package dev.mednikov.social.connections.messaging; + +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; + +import java.util.function.Function; + +final class UserParamsMapper implements Function { + + @Override + public MapSqlParameterSource apply(UserPayload user) { + MapSqlParameterSource sqlParameterSource = new MapSqlParameterSource(); + sqlParameterSource.addValue("id", Long.valueOf(user.getId())); + sqlParameterSource.addValue("first_name", user.getFirstName()); + sqlParameterSource.addValue("last_name", user.getLastName()); + sqlParameterSource.addValue("email", user.getEmail()); + sqlParameterSource.addValue("is_active", user.isActive()); + sqlParameterSource.addValue("is_onboarded", user.isOnboarded()); + sqlParameterSource.addValue("is_email_verified", user.isEmailVerified()); + sqlParameterSource.addValue("avatar_url", user.getAvatarUrl()); + sqlParameterSource.addValue("headline", user.getHeadline()); + sqlParameterSource.addValue("version", user.getVersion()); + return sqlParameterSource; + } +} diff --git a/src/main/java/dev/mednikov/social/connections/messaging/UserPayload.java b/src/main/java/dev/mednikov/social/connections/messaging/UserPayload.java new file mode 100644 index 0000000..0fb5780 --- /dev/null +++ b/src/main/java/dev/mednikov/social/connections/messaging/UserPayload.java @@ -0,0 +1,112 @@ +package dev.mednikov.social.connections.messaging; + +import dev.mednikov.social.connections.models.User; + +final class UserPayload { + + private String id; + private String firstName; + private String lastName; + private String email; + private String avatarUrl; + private boolean emailVerified; + private boolean active; + private boolean onboarded; + private String headline; + private Long version; + + UserPayload() {} + + UserPayload(User user) { + this.id = user.getId().toString(); + this.firstName = user.getFirstName(); + this.lastName = user.getLastName(); + this.email = user.getEmail(); + this.avatarUrl = user.getAvatarUrl(); + this.emailVerified = user.getEmailVerified(); + this.active = user.getActive(); + this.onboarded = user.getOnboarded(); + this.headline = user.getHeadline(); + this.version = user.getVersion(); + } + + String getId() { + return id; + } + + String getFirstName() { + return firstName; + } + + String getLastName() { + return lastName; + } + + String getEmail() { + return email; + } + + String getAvatarUrl() { + return avatarUrl; + } + + boolean isEmailVerified() { + return emailVerified; + } + + boolean isActive() { + return active; + } + + boolean isOnboarded() { + return onboarded; + } + + String getHeadline() { + return headline; + } + + Long getVersion() { + return version; + } + + public void setId(String id) { + this.id = id; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public void setEmail(String email) { + this.email = email; + } + + public void setAvatarUrl(String avatarUrl) { + this.avatarUrl = avatarUrl; + } + + public void setEmailVerified(boolean emailVerified) { + this.emailVerified = emailVerified; + } + + public void setActive(boolean active) { + this.active = active; + } + + public void setOnboarded(boolean onboarded) { + this.onboarded = onboarded; + } + + public void setHeadline(String headline) { + this.headline = headline; + } + + public void setVersion(Long version) { + this.version = version; + } +} diff --git a/src/main/java/dev/mednikov/social/connections/messaging/UserUpdatedListener.java b/src/main/java/dev/mednikov/social/connections/messaging/UserUpdatedListener.java new file mode 100644 index 0000000..22dcf9d --- /dev/null +++ b/src/main/java/dev/mednikov/social/connections/messaging/UserUpdatedListener.java @@ -0,0 +1,52 @@ +package dev.mednikov.social.connections.messaging; + +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.mednikov.social.connections.models.User; +import jakarta.transaction.Transactional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.stereotype.Component; + +@Component +public class UserUpdatedListener { + + private final static UserParamsMapper userParamsMapper = new UserParamsMapper(); + private final static Logger logger = LoggerFactory.getLogger(UserUpdatedListener.class); + + private final NamedParameterJdbcTemplate jdbcTemplate; + private final ObjectMapper objectMapper; + + public UserUpdatedListener(NamedParameterJdbcTemplate jdbcTemplate, ObjectMapper objectMapper) { + this.jdbcTemplate = jdbcTemplate; + this.objectMapper = objectMapper; + } + + @Transactional + @RabbitListener(queues = {"social_users_updated"}) + public void onEvent (String payload){ + logger.info("User updated" + payload); + try { + UserPayload user = this.objectMapper.readValue(payload, UserPayload.class); + String query = """ + UPDATE connections_user + SET first_name = :first_name, + last_name = :last_name, + email = :email, + is_active = :is_active, + is_email_verified = :is_email_verified, + is_onboarded = :is_onboarded, + avatar_url = :avatar_url, + headline = :headline, + version = :version + WHERE id = :id + """; + MapSqlParameterSource sqlParameterSource = userParamsMapper.apply(user); + jdbcTemplate.update(query, sqlParameterSource); + } catch (Exception ex){ + logger.error(ex.getMessage()); + } + } +} diff --git a/src/main/java/dev/mednikov/social/connections/models/User.java b/src/main/java/dev/mednikov/social/connections/models/User.java new file mode 100644 index 0000000..46ffe12 --- /dev/null +++ b/src/main/java/dev/mednikov/social/connections/models/User.java @@ -0,0 +1,117 @@ +package dev.mednikov.social.connections.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + + +@Entity +@Table(name = "connections_user") +public class User { + + @Id private Long id; + @Column(nullable = false, name = "email") private String email; + @Column(nullable = false, name = "first_name") private String firstName; + @Column(nullable = false, name = "last_name") private String lastName; + @Column(nullable = false, name = "avatar_url") private String avatarUrl; + @Column(nullable = false, name = "headline") private String headline; + @Column(nullable = false, name = "is_active") private Boolean active; + @Column(nullable = false, name = "is_email_verified") private Boolean emailVerified; + @Column(nullable = false, name = "is_onboarded") private Boolean onboarded; + @Column(nullable = false, name = "version") private Long version; + + @Override + public final boolean equals(Object o) { + if (!(o instanceof User user)) return false; + + return id.equals(user.id); + } + + @Override + public int hashCode() { + return id.hashCode(); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public Boolean getOnboarded() { + return onboarded; + } + + public void setOnboarded(Boolean onboarded) { + this.onboarded = onboarded; + } + + public Boolean getEmailVerified() { + return emailVerified; + } + + public void setEmailVerified(Boolean emailVerified) { + this.emailVerified = emailVerified; + } + + public String getAvatarUrl() { + return avatarUrl; + } + + public void setAvatarUrl(String avatarUrl) { + this.avatarUrl = avatarUrl; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getHeadline() { + return headline; + } + + public void setHeadline(String headline) { + this.headline = headline; + } + + public Boolean getActive() { + return active; + } + + public void setActive(Boolean active) { + this.active = active; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + +} diff --git a/src/main/java/dev/mednikov/social/connections/repositories/IdentifierGenerator.java b/src/main/java/dev/mednikov/social/connections/repositories/IdentifierGenerator.java new file mode 100644 index 0000000..11bb3d7 --- /dev/null +++ b/src/main/java/dev/mednikov/social/connections/repositories/IdentifierGenerator.java @@ -0,0 +1,7 @@ +package dev.mednikov.social.connections.repositories; + +public interface IdentifierGenerator { + + Long getNextId(); + +} diff --git a/src/main/java/dev/mednikov/social/connections/repositories/IdentifierGeneratorImpl.java b/src/main/java/dev/mednikov/social/connections/repositories/IdentifierGeneratorImpl.java new file mode 100644 index 0000000..bb96e26 --- /dev/null +++ b/src/main/java/dev/mednikov/social/connections/repositories/IdentifierGeneratorImpl.java @@ -0,0 +1,17 @@ +package dev.mednikov.social.connections.repositories; + +import cn.hutool.core.lang.generator.SnowflakeGenerator; + +public class IdentifierGeneratorImpl implements IdentifierGenerator { + + private final SnowflakeGenerator snowflakeGenerator; + + public IdentifierGeneratorImpl() { + this.snowflakeGenerator = new SnowflakeGenerator(); + } + + @Override + public Long getNextId() { + return this.snowflakeGenerator.next(); + } +} diff --git a/src/main/java/dev/mednikov/social/connections/repositories/UserRepository.java b/src/main/java/dev/mednikov/social/connections/repositories/UserRepository.java new file mode 100644 index 0000000..56dd426 --- /dev/null +++ b/src/main/java/dev/mednikov/social/connections/repositories/UserRepository.java @@ -0,0 +1,8 @@ +package dev.mednikov.social.connections.repositories; + +import dev.mednikov.social.connections.models.User; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepository extends JpaRepository { + +} diff --git a/src/main/resources/db/migration/V1__initial.sql b/src/main/resources/db/migration/V1__initial.sql new file mode 100644 index 0000000..be121e7 --- /dev/null +++ b/src/main/resources/db/migration/V1__initial.sql @@ -0,0 +1,12 @@ +CREATE TABLE IF NOT EXISTS connections_user ( + id BIGINT PRIMARY KEY, + first_name VARCHAR(255) NOT NULL, + last_name VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL, + avatar_url VARCHAR(255) NOT NULL, + is_active BOOLEAN NOT NULL, + is_onboarded BOOLEAN NOT NULL, + is_email_verified BOOLEAN NOT NULL, + headline VARCHAR(255) NOT NULL, + version BIGINT NOT NULL +); \ No newline at end of file