diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..28d595e --- /dev/null +++ b/pom.xml @@ -0,0 +1,93 @@ + + 4.0.0 + + com.example + my-project + 1.0-SNAPSHOT + + + 8 + 8 + + + + + + mysql + mysql-connector-java + 8.0.30 + + + + org.hibernate + hibernate-core-jakarta + 5.6.14.Final + + + + p6spy + p6spy + 3.9.1 + + + + io.lettuce + lettuce-core + 6.2.2.RELEASE + + + + org.projectlombok + lombok + 1.18.30 + provided + + + + org.mockito + mockito-core + 5.12.0 + test + + + + org.mockito + mockito-junit-jupiter + 5.12.0 + test + + + + org.slf4j + slf4j-api + 2.0.13 + + + + com.fasterxml.jackson.core + jackson-databind + 2.14.0 + + + redis.clients + jedis + 5.2.0 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 21 + 21 + + + + + diff --git a/src/main/java/com/javarush/Application.java b/src/main/java/com/javarush/Application.java new file mode 100644 index 0000000..56c48c5 --- /dev/null +++ b/src/main/java/com/javarush/Application.java @@ -0,0 +1,124 @@ +package com.javarush; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.javarush.cache.RedisRepository; +import com.javarush.config.HibernateUtil; +import com.javarush.config.RedisConfig; +import com.javarush.domain.entity.City; +import com.javarush.domain.entity.CountryLanguage; +import com.javarush.redis.CityCountry; +import com.javarush.repository.CityRepository; +import com.javarush.repository.CountryRepository; +import com.javarush.services.CityService; +import com.javarush.services.CountryService; +import io.lettuce.core.RedisClient; +import io.lettuce.core.api.StatefulRedisConnection; +import io.lettuce.core.api.sync.RedisStringCommands; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import static java.util.Objects.nonNull; + +public class Application { + public final SessionFactory sessionFactory; + public final RedisClient redisClient; + public final CityService cityService; + public final CountryService countryService; + public final ObjectMapper mapper; + private static final Logger LOGGER = LoggerFactory.getLogger(Application.class); + + public Application() { + sessionFactory = HibernateUtil.getSessionFactory(); + redisClient = RedisConfig.prepareRedisClient(); + cityService = new CityService(new RedisRepository(), new CityRepository()); + countryService = new CountryService(new RedisRepository(), new CountryRepository()); + mapper = new ObjectMapper(); + } + + private void shutdown() { + if (nonNull(sessionFactory)) { + sessionFactory.close(); + } + if (nonNull(redisClient)) { + redisClient.shutdown(); + } + } + + public static void main(String[] args) { + int numOfQueries = 15; + Application application = new Application(); + RedisRepository countryRedisRepository = new RedisRepository(); + RedisRepository cityRedisRepository = new RedisRepository(); + + System.out.println("Querying Country by ID..."); + for (int i = 0; i < numOfQueries; i++) { + application.countryService.getById(1); + application.cityService.getById(2); + } + } + + private void pushToRedis(List data) { + try (StatefulRedisConnection connection = redisClient.connect()) { + RedisStringCommands sync = connection.sync(); + for (CityCountry cityCountry : data) { + try { + sync.set(String.valueOf(cityCountry.getId()), mapper.writeValueAsString(cityCountry)); + } catch (JsonProcessingException e) { + LOGGER.error("Couldn't push to redis : ", e); + e.printStackTrace(System.out); + } + } + + } + } + + private void testRedisData(List ids) { + try (StatefulRedisConnection connection = redisClient.connect()) { + RedisStringCommands sync = connection.sync(); + for (Integer id : ids) { + String value = sync.get(String.valueOf(id)); + try { + mapper.readValue(value, CityCountry.class); + } catch (JsonProcessingException e) { + LOGGER.error("Couldn't test redis data : ", e); + e.printStackTrace(System.out); + } + } + } + } + + public void testMysqlData(List ids) { + try (Session session = sessionFactory.getCurrentSession()) { + session.beginTransaction(); + for (Integer id : ids) { + City city = cityService.getById(id); + Set languages = city.getCountryId().getLanguages(); + } + session.getTransaction().commit(); + } + } + + public List fetchData() { + try (Session session = sessionFactory.getCurrentSession()) { + List allCities = new ArrayList<>(); + session.beginTransaction(); + + countryService.getAll(); + + int totalCount = cityService.getTotalCount(); + int step = 500; + for (int i = 0; i < totalCount; i += step) { + allCities.addAll(cityService.getItems(i, step)); + } + session.getTransaction().commit(); + return allCities; + } + } +} diff --git a/src/main/java/com/javarush/DataTransformer.java b/src/main/java/com/javarush/DataTransformer.java new file mode 100644 index 0000000..9803fe7 --- /dev/null +++ b/src/main/java/com/javarush/DataTransformer.java @@ -0,0 +1,72 @@ +package com.javarush; + +import com.javarush.domain.entity.City; +import com.javarush.domain.entity.Country; +import com.javarush.domain.entity.CountryLanguage; +import com.javarush.redis.CityCountry; +import com.javarush.redis.Language; + +import java.util.Set; +import java.util.stream.Collectors; + +public class DataTransformer { + public static CityCountry countryTransformToCityCountry(Country country) { + CityCountry res = new CityCountry(); + res.setAlternativeCountryCode(country.getCode2()); + res.setContinent(country.getContinent()); + res.setCountryCode(country.getCode()); + res.setCountryName(country.getName()); + res.setCountryPopulation(country.getPopulation()); + res.setCountryRegion(country.getRegion()); + res.setCountrySurfaceArea(country.getSurfaceArea()); + Set countryLanguages = country.getLanguages(); + Set languages = countryLanguages.stream().map(cl -> { + Language language = new Language(); + language.setLanguage(cl.getLanguage()); + language.setIsOfficial(cl.getIsOfficial()); + language.setPercentage(cl.getPercentage()); + return language; + }).collect(Collectors.toSet()); + res.setLanguages(languages); + return res; + } + + public static CityCountry cityTransformToCityCountry(City city) { + CityCountry res = new CityCountry(); + res.setId(city.getId()); + res.setName(city.getName()); + res.setPopulation(city.getPopulation()); + res.setDistrict(city.getDistrict()); + return res; + } + + public static City cityCountryTransformToCity(CityCountry cityCountry) { + City city = new City(); + city.setId(cityCountry.getId()); + city.setName(cityCountry.getName()); + city.setPopulation(cityCountry.getPopulation()); + city.setDistrict(cityCountry.getDistrict()); + return city; + } + + public static Country cityCountryToCountry(CityCountry cityCountry) { + Country country = new Country(); + country.setCode2(cityCountry.getAlternativeCountryCode()); + country.setContinent(cityCountry.getContinent()); + country.setCode(cityCountry.getCountryCode()); + country.setName(cityCountry.getCountryName()); + country.setPopulation(cityCountry.getCountryPopulation()); + country.setRegion(cityCountry.getCountryRegion()); + country.setSurfaceArea(cityCountry.getCountrySurfaceArea()); + Set languages = cityCountry.getLanguages(); + Set countryLanguages = languages.stream().map(cl -> { + CountryLanguage countryLanguage = new CountryLanguage(); + countryLanguage.setLanguage(cl.getLanguage()); + countryLanguage.setIsOfficial(cl.getIsOfficial()); + countryLanguage.setPercentage(cl.getPercentage()); + return countryLanguage; + }).collect(Collectors.toSet()); + country.setLanguages(countryLanguages); + return country; + } +} diff --git a/src/main/java/com/javarush/cache/RedisRepository.java b/src/main/java/com/javarush/cache/RedisRepository.java new file mode 100644 index 0000000..4803997 --- /dev/null +++ b/src/main/java/com/javarush/cache/RedisRepository.java @@ -0,0 +1,58 @@ +package com.javarush.cache; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.javarush.services.CityService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import redis.clients.jedis.UnifiedJedis; +import redis.clients.jedis.util.SafeEncoder; + +public class RedisRepository { + private final UnifiedJedis jedis; + private final ObjectMapper objectMapper = new ObjectMapper(); + private static final String TOPK_NAME = "city-country"; + private static final int THRESHOLD = 7; + private static final Logger LOGGER = LoggerFactory.getLogger(CityService.class); + private static final String REDIS_URL = "redis://127.0.0.1:6379"; + private static final String BRACES_REGEX = "[\\[\\]]"; + + public RedisRepository() { + jedis = new UnifiedJedis(REDIS_URL); + + jedis.del(TOPK_NAME); + if (!jedis.exists(TOPK_NAME)) { + jedis.topkReserve(TOPK_NAME, 5L, 2000L, 7L, 0.925D); + } + } + + public T getById(String key, Class clazz) throws JsonProcessingException { + String serializedEntity = jedis.get(key); + return deserialize(serializedEntity, clazz); + } + + public void put(String key, T value) throws JsonProcessingException { + jedis.topkAdd(TOPK_NAME, key); + + if (getCount(key) >= THRESHOLD) { + jedis.set(key, serialize(value)); + } + } + + private int getCount(String key) { + String count = jedis.sendCommand(() -> SafeEncoder.encode("TOPK.COUNT"), SafeEncoder.encodeMany(TOPK_NAME, key)).toString(); + return Integer.parseInt(count.replaceAll(BRACES_REGEX, "")); + } + + public boolean checkExists(String key) { + return jedis.exists(key); + } + + private String serialize(T entity) throws JsonProcessingException { + return objectMapper.writeValueAsString(entity); + } + + private T deserialize(String serializedEntity, Class clazz) throws JsonProcessingException { + return objectMapper.readValue(serializedEntity, clazz); + } +} diff --git a/src/main/java/com/javarush/config/HibernateUtil.java b/src/main/java/com/javarush/config/HibernateUtil.java new file mode 100644 index 0000000..ed23ee1 --- /dev/null +++ b/src/main/java/com/javarush/config/HibernateUtil.java @@ -0,0 +1,46 @@ +package com.javarush.config; + +import com.javarush.domain.entity.City; +import com.javarush.domain.entity.Country; +import com.javarush.domain.entity.CountryLanguage; +import org.hibernate.SessionFactory; +import org.hibernate.cfg.Configuration; +import org.hibernate.cfg.Environment; + +import java.util.Properties; + +public class HibernateUtil { + private static SessionFactory sessionFactory; + + private HibernateUtil() { + + } + + private static SessionFactory prepareRelationalDb() { + final SessionFactory sessionFactory; + Properties properties = new Properties(); + properties.put(Environment.DIALECT, "org.hibernate.dialect.MySQL8Dialect"); + properties.put(Environment.DRIVER, "com.p6spy.engine.spy.P6SpyDriver"); + properties.put(Environment.URL, "jdbc:p6spy:mysql://localhost:3306/world"); + properties.put(Environment.USER, "root"); + properties.put(Environment.PASS, "root"); + properties.put(Environment.CURRENT_SESSION_CONTEXT_CLASS, "thread"); + properties.put(Environment.HBM2DDL_AUTO, "validate"); + properties.put(Environment.STATEMENT_BATCH_SIZE, "100"); + + sessionFactory = new Configuration() + .addAnnotatedClass(City.class) + .addAnnotatedClass(Country.class) + .addAnnotatedClass(CountryLanguage.class) + .addProperties(properties) + .buildSessionFactory(); + return sessionFactory; + } + + public static SessionFactory getSessionFactory() { + if (sessionFactory == null) { + sessionFactory = prepareRelationalDb(); + } + return sessionFactory; + } +} diff --git a/src/main/java/com/javarush/config/RedisConfig.java b/src/main/java/com/javarush/config/RedisConfig.java new file mode 100644 index 0000000..6e4468e --- /dev/null +++ b/src/main/java/com/javarush/config/RedisConfig.java @@ -0,0 +1,15 @@ +package com.javarush.config; + +import io.lettuce.core.RedisClient; +import io.lettuce.core.RedisURI; +import io.lettuce.core.api.StatefulRedisConnection; + +public class RedisConfig { + public static RedisClient prepareRedisClient() { + RedisClient redisClient = RedisClient.create(RedisURI.create("localhost", 6379)); + try (StatefulRedisConnection connection = redisClient.connect()) { + System.out.println("\nConnected to Redis\n"); + } + return redisClient; + } +} diff --git a/src/main/java/com/javarush/domain/entity/City.java b/src/main/java/com/javarush/domain/entity/City.java new file mode 100644 index 0000000..0a7f00f --- /dev/null +++ b/src/main/java/com/javarush/domain/entity/City.java @@ -0,0 +1,26 @@ +package com.javarush.domain.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@Entity +@Table(name = "city") +public class City { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + @ManyToOne + @JoinColumn(name = "country_id") + private Country countryId; + @Column(name = "name") + private String name; + @Column(name = "district") + private String district; + @Column(name = "population") + private Integer population; +} diff --git a/src/main/java/com/javarush/domain/Country.java b/src/main/java/com/javarush/domain/entity/Country.java similarity index 82% rename from src/main/java/com/javarush/domain/Country.java rename to src/main/java/com/javarush/domain/entity/Country.java index c2194c3..9f5eedc 100644 --- a/src/main/java/com/javarush/domain/Country.java +++ b/src/main/java/com/javarush/domain/entity/Country.java @@ -1,18 +1,23 @@ -package com.javarush.domain; +package com.javarush.domain.entity; +import com.javarush.domain.enums.Continent; import jakarta.persistence.*; -import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; import java.math.BigDecimal; import java.util.Set; -@Data +@Getter +@Setter +@NoArgsConstructor @Entity @Table(name = "country") public class Country { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private int id; + private Integer id; @Column(name = "code") private String code; @Column(name = "code_2") @@ -27,9 +32,9 @@ public class Country { @Column(name = "surface_area") private BigDecimal surfaceArea; @Column(name = "indep_year") - private short indepYear; + private Short indepYear; @Column(name = "population") - private int population; + private Integer population; @Column(name = "life_expectancy") private BigDecimal lifeExpectancy; @Column(name = "gnp") diff --git a/src/main/java/com/javarush/domain/entity/CountryLanguage.java b/src/main/java/com/javarush/domain/entity/CountryLanguage.java new file mode 100644 index 0000000..9fc0514 --- /dev/null +++ b/src/main/java/com/javarush/domain/entity/CountryLanguage.java @@ -0,0 +1,31 @@ +package com.javarush.domain.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.hibernate.annotations.Type; + +import java.math.BigDecimal; + +@Getter +@Setter +@NoArgsConstructor +@Entity +@Table(name = "country_language") +public class CountryLanguage { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + @ManyToOne + @JoinColumn(name = "country_id") + private Country countryId; + @Column(name = "language") + private String language; + @Column(name = "is_official", columnDefinition = "BIT") + @Type(type = "org.hibernate.type.NumericBooleanType") + private Boolean isOfficial; + @Column(name = "percentage") + private BigDecimal percentage; + +} diff --git a/src/main/java/com/javarush/domain/enums/Continent.java b/src/main/java/com/javarush/domain/enums/Continent.java new file mode 100644 index 0000000..7d8b583 --- /dev/null +++ b/src/main/java/com/javarush/domain/enums/Continent.java @@ -0,0 +1,11 @@ +package com.javarush.domain.enums; + +public enum Continent { + ASIA, + EUROPE, + NORTH_AMERICA, + AFRICA, + OCEANIA, + ANTARCTICA, + SOUTH_AMERICA +} diff --git a/src/main/java/com/javarush/domain/exceptions/CityException.java b/src/main/java/com/javarush/domain/exceptions/CityException.java new file mode 100644 index 0000000..b5efd0b --- /dev/null +++ b/src/main/java/com/javarush/domain/exceptions/CityException.java @@ -0,0 +1,7 @@ +package com.javarush.domain.exceptions; + +public class CityException extends RuntimeException { + public CityException(String message) { + super(message); + } +} diff --git a/src/main/java/com/javarush/domain/exceptions/CountryException.java b/src/main/java/com/javarush/domain/exceptions/CountryException.java new file mode 100644 index 0000000..6f278c1 --- /dev/null +++ b/src/main/java/com/javarush/domain/exceptions/CountryException.java @@ -0,0 +1,7 @@ +package com.javarush.domain.exceptions; + +public class CountryException extends RuntimeException { + public CountryException(String message) { + super(message); + } +} diff --git a/src/main/java/com/javarush/domain/exceptions/RedisException.java b/src/main/java/com/javarush/domain/exceptions/RedisException.java new file mode 100644 index 0000000..41ace4d --- /dev/null +++ b/src/main/java/com/javarush/domain/exceptions/RedisException.java @@ -0,0 +1,7 @@ +package com.javarush.domain.exceptions; + +public class RedisException extends RuntimeException { + public RedisException(String message) { + super(message); + } +} diff --git a/src/main/java/com/javarush/redis/CityCountry.java b/src/main/java/com/javarush/redis/CityCountry.java new file mode 100644 index 0000000..5f64121 --- /dev/null +++ b/src/main/java/com/javarush/redis/CityCountry.java @@ -0,0 +1,34 @@ +package com.javarush.redis; + +import com.javarush.domain.enums.Continent; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Set; + +@Data +public class CityCountry { + private Integer id; + + private String name; + + private String district; + + private Integer population; + + private String countryCode; + + private String alternativeCountryCode; + + private String countryName; + + private Continent continent; + + private String countryRegion; + + private BigDecimal countrySurfaceArea; + + private Integer countryPopulation; + + private Set languages; +} diff --git a/src/main/java/com/javarush/redis/Language.java b/src/main/java/com/javarush/redis/Language.java new file mode 100644 index 0000000..c91d80a --- /dev/null +++ b/src/main/java/com/javarush/redis/Language.java @@ -0,0 +1,12 @@ +package com.javarush.redis; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class Language { + private String language; + private Boolean isOfficial; + private BigDecimal percentage; +} diff --git a/src/main/java/com/javarush/repository/CityRepository.java b/src/main/java/com/javarush/repository/CityRepository.java new file mode 100644 index 0000000..0420776 --- /dev/null +++ b/src/main/java/com/javarush/repository/CityRepository.java @@ -0,0 +1,67 @@ +package com.javarush.repository; + +import com.javarush.config.HibernateUtil; +import com.javarush.domain.entity.City; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.Transaction; + +import java.util.List; + +public class CityRepository implements CrudRepository { + private final SessionFactory sessionFactory = HibernateUtil.getSessionFactory(); + + @Override + public City getById(Integer id) { + try (Session session = sessionFactory.openSession()) { + return session.createQuery("select c from City c join fetch c.countryId where c.id = :ID", City.class) + .setParameter("ID", id) + .uniqueResult(); + } + } + + @Override + public City save(City entity) { + try (Session session = sessionFactory.getCurrentSession()) { + Transaction transaction = session.beginTransaction(); + session.persist(entity); + transaction.commit(); + return entity; + } + } + + @Override + public void delete(Integer id) { + try (Session session = sessionFactory.getCurrentSession()) { + Transaction transaction = session.beginTransaction(); + session.createQuery("delete from City c where c.id = :ID", City.class) + .setParameter("ID", id) + .executeUpdate(); + transaction.commit(); + } + } + + @Override + public List getAll() { + try (Session session = sessionFactory.openSession()) { + return session.createQuery("select c from City c", City.class).list(); + } + } + + @Override + public List getItems(int offset, int limit) { + try(Session session = sessionFactory.openSession()) { + return session.createQuery("select c from City c", City.class) + .setFirstResult(offset) + .setMaxResults(limit) + .list(); + } + } + + @Override + public int getTotalCount() { + try(Session session = sessionFactory.openSession()) { + return Math.toIntExact(session.createQuery("select count(c) from City c", Long.class).uniqueResult()); + } + } +} diff --git a/src/main/java/com/javarush/repository/CountryRepository.java b/src/main/java/com/javarush/repository/CountryRepository.java new file mode 100644 index 0000000..7207b59 --- /dev/null +++ b/src/main/java/com/javarush/repository/CountryRepository.java @@ -0,0 +1,66 @@ +package com.javarush.repository; + +import com.javarush.config.HibernateUtil; +import com.javarush.domain.entity.Country; +import org.hibernate.Session; +import org.hibernate.SessionFactory; + +import java.util.List; + +public class CountryRepository implements CrudRepository { + private final SessionFactory sessionFactory = HibernateUtil.getSessionFactory(); + + @Override + public Country getById(Integer id) { + try (Session session = sessionFactory.openSession()) { + return session.createQuery("select c from Country c where c.id = :ID", Country.class) + .setParameter("ID", id) + .getSingleResult(); + } + } + + @Override + public Country save(Country entity) { + try (Session session = sessionFactory.getCurrentSession()) { + session.beginTransaction(); + session.persist(entity); + session.getTransaction().commit(); + return entity; + } + } + + @Override + public void delete(Integer id) { + try (Session session = sessionFactory.getCurrentSession()) { + session.beginTransaction(); + session.createQuery("delete from Country c where c.id = :ID", Country.class) + .setParameter("ID", id) + .executeUpdate(); + session.getTransaction().commit(); + } + } + + @Override + public List getAll() { + try (Session session = sessionFactory.openSession()) { + return session.createQuery("select c from Country c join fetch c.languages", Country.class).list(); + } + } + + @Override + public List getItems(int offset, int limit) { + try (Session session = sessionFactory.getCurrentSession()) { + return session.createQuery("select c from Country c", Country.class) + .setFirstResult(offset) + .setMaxResults(limit) + .list(); + } + } + + @Override + public int getTotalCount() { + try (Session session = sessionFactory.getCurrentSession()) { + return Math.toIntExact(session.createQuery("select count(c) from Country c", Long.class).uniqueResult()); + } + } +} diff --git a/src/main/java/com/javarush/repository/CrudRepository.java b/src/main/java/com/javarush/repository/CrudRepository.java new file mode 100644 index 0000000..4461b32 --- /dev/null +++ b/src/main/java/com/javarush/repository/CrudRepository.java @@ -0,0 +1,17 @@ +package com.javarush.repository; + +import java.util.List; + +public interface CrudRepository { + T getById(I id); + + T save(T entity); + + void delete(I id); + + List getAll(); + + List getItems(int offset, int limit); + + int getTotalCount(); +} diff --git a/src/main/java/com/javarush/services/CityService.java b/src/main/java/com/javarush/services/CityService.java new file mode 100644 index 0000000..86d90fa --- /dev/null +++ b/src/main/java/com/javarush/services/CityService.java @@ -0,0 +1,101 @@ +package com.javarush.services; + +import com.javarush.DataTransformer; +import com.javarush.cache.RedisRepository; +import com.javarush.domain.entity.City; +import com.javarush.domain.exceptions.CityException; +import com.javarush.redis.CityCountry; +import com.javarush.repository.CityRepository; +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +@RequiredArgsConstructor +public class CityService { + private static final Logger LOGGER = LoggerFactory.getLogger(CityService.class); + private final RedisRepository redisRepository; + private final CityRepository cityRepository; + + public City getById(Integer id) { + String key = "city_" + id; + try { + if(redisRepository.checkExists(key)){ + CityCountry cityCountry = redisRepository.getById(key, CityCountry.class); + return DataTransformer.cityCountryTransformToCity(cityCountry); + } + City city = cityRepository.getById(id); + redisRepository.put(key, DataTransformer.cityTransformToCityCountry(city)); + return city; + } catch (Exception e) { + LOGGER.error("Cannot get city with id : {} ", id, e); + throw new CityException("ERROR :: cannot get city with id " + id + " " + e.getMessage()); + } + } + + public City save(City entity) { + try { + City city = new CityRepository().save(entity); + if (city == null) { + LOGGER.error("No city with id {}", entity.getId()); + throw new CityException("ERROR :: no city with id " + entity.getId()); + } + return city; + } catch (Exception e) { + LOGGER.error("Cannot save city with id {}", entity.getId(), e); + throw new CityException("ERROR :: cannot save city with id " + entity.getId() + " " + e.getMessage()); + } + } + + public void delete(Integer id) { + try { + new CityRepository().delete(id); + } catch (Exception e) { + LOGGER.error("Cannot delete city with id: {}", id, e); + throw new CityException("ERROR :: cannot delete city with id: " + id + " " + e.getMessage()); + } + } + + public List getAll() { + try { + List cities = new CityRepository().getAll(); + if (cities.isEmpty()) { + LOGGER.error("No cities found"); + throw new CityException("ERROR :: no cities found"); + } + return cities; + } catch (Exception e) { + LOGGER.error("Cannot get all cities : ", e); + throw new CityException("ERROR :: cannot get all cities: " + e.getMessage()); + } + } + + public List getItems(int offset, int limit) { + try { + List cities = new CityRepository().getItems(offset, limit); + if (cities.isEmpty()) { + LOGGER.error("No cities found in range"); + throw new CityException("ERROR :: no cities found in range"); + } + return cities; + } catch (Exception e) { + LOGGER.error("Cannot get all cities in range : ", e); + throw new CityException("ERROR :: cannot get all cities in range: " + e.getMessage()); + } + } + + public int getTotalCount() { + try { + int totalCount = new CityRepository().getTotalCount(); + if (totalCount == 0) { + LOGGER.error("Total cities count is 0"); + throw new CityException("ERROR :: total cities count is 0"); + } + return totalCount; + } catch (Exception e) { + LOGGER.error("Cannot get all cities count : ", e); + throw new CityException("ERROR :: cannot get all cities count: " + e.getMessage()); + } + } +} diff --git a/src/main/java/com/javarush/services/CountryService.java b/src/main/java/com/javarush/services/CountryService.java new file mode 100644 index 0000000..ba244b0 --- /dev/null +++ b/src/main/java/com/javarush/services/CountryService.java @@ -0,0 +1,101 @@ +package com.javarush.services; + +import com.javarush.DataTransformer; +import com.javarush.cache.RedisRepository; +import com.javarush.domain.entity.Country; +import com.javarush.domain.exceptions.CountryException; +import com.javarush.redis.CityCountry; +import com.javarush.repository.CountryRepository; +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +@RequiredArgsConstructor +public class CountryService { + private static final Logger LOGGER = LoggerFactory.getLogger(CountryService.class); + private final RedisRepository redisRepository; + private final CountryRepository countryRepository; + + public Country getById(Integer id) { + String key = "country_" + id; + try { + if (redisRepository.checkExists(key)) { + CityCountry cityCountry = redisRepository.getById(key, CityCountry.class); + return DataTransformer.cityCountryToCountry(cityCountry); + } + Country country = countryRepository.getById(id); + redisRepository.put(key, DataTransformer.countryTransformToCityCountry(country)); + return country; + } catch (Exception e) { + LOGGER.error("Cannot get country with id {}", id, e); + throw new CountryException("ERROR :: cannot get country with id " + id + " " + e.getMessage()); + } + } + + public Country save(Country entity) { + try { + Country savedCountry = countryRepository.save(entity); + if (savedCountry == null) { + LOGGER.error("Failed to save country with id {}", entity.getId()); + throw new CountryException("ERROR :: no country with id " + entity.getId()); + } + return savedCountry; + } catch (Exception ex) { + LOGGER.error("Could not save country with id {}", entity.getId(), ex); + throw new CountryException("ERROR :: could not save country with id " + entity.getId() + " " + ex.getMessage()); + } + } + + public void delete(Integer id) { + try { + countryRepository.delete(id); + } catch (Exception e) { + LOGGER.error("Cannot delete country with id: {}", id, e); + throw new CountryException("ERROR :: cannot delete country with id: " + id + " " + e.getMessage()); + } + } + + public List getAll() { + try { + List countries = countryRepository.getAll(); + if (countries.isEmpty()) { + LOGGER.error("No countries found"); + throw new CountryException("ERROR :: no countries found"); + } + return countries; + } catch (Exception e) { + LOGGER.error("Cannot get all countries : ", e); + throw new CountryException("ERROR :: cannot get all countries: " + e.getMessage()); + } + } + + public List getItems(int offset, int limit) { + try { + List countries = countryRepository.getItems(offset, limit); + if (countries.isEmpty()) { + LOGGER.error("No countries found in range"); + throw new CountryException("ERROR :: no countries found in range"); + } + return countries; + } catch (Exception e) { + LOGGER.error("Cannot get all countries in range : ", e); + throw new CountryException("ERROR :: cannot get all countries in range: " + e.getMessage()); + } + } + + public int getTotalCount() { + try { + int totalCount = countryRepository.getTotalCount(); + if (totalCount == 0) { + LOGGER.error("Total countries count is 0"); + throw new CountryException("ERROR :: total countries count is 0"); + } + return totalCount; + } catch (Exception e) { + LOGGER.error("Cannot get all countries count : ", e); + throw new CountryException("ERROR :: cannot get all countries count: " + e.getMessage()); + } + } +} diff --git a/src/test/java/com/javarush/services/CityServiceTest.java b/src/test/java/com/javarush/services/CityServiceTest.java new file mode 100644 index 0000000..f90b4a8 --- /dev/null +++ b/src/test/java/com/javarush/services/CityServiceTest.java @@ -0,0 +1,116 @@ +package com.javarush.services; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.javarush.DataTransformer; +import com.javarush.cache.RedisRepository; +import com.javarush.domain.entity.City; +import com.javarush.domain.exceptions.CityException; +import com.javarush.redis.CityCountry; +import com.javarush.repository.CityRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class CityServiceTest { + + @Mock + private RedisRepository redisRepository; + + @Mock + private CityRepository cityRepository; + + @InjectMocks + private CityService cityService; + + private City city; + + @BeforeEach + void setUp() { + MockitoAnnotations.initMocks(this); + city = new City(); + } + + @Test + void testGetById_CityExistsInRedis() throws JsonProcessingException { + int id = 1; + String key = "city_" + id; + CityCountry testCityCountry = DataTransformer.cityTransformToCityCountry(city); + + when(redisRepository.checkExists(key)).thenReturn(true); + when(redisRepository.getById(key, CityCountry.class)).thenReturn(testCityCountry); + + City city = cityService.getById(id); + + assertNotNull(city); + assertEquals(city.getId(), city.getId()); + verify(redisRepository, times(1)).getById(key, CityCountry.class); + } + + @Test + void testGetById_CityNotInRedis() throws Exception { + int cityId = 1; + String key = "city_" + cityId; + + when(redisRepository.checkExists(key)).thenReturn(false); + when(cityRepository.getById(cityId)).thenReturn(city); + + City result = cityService.getById(cityId); + + assertNotNull(result); + verify(redisRepository, times(1)).put(eq(key), any(CityCountry.class)); + assertEquals(city, result); + } + + @Test + void testGetById_ExceptionThrown() { + int cityId = 1; + String key = "city_" + cityId; + + when(redisRepository.checkExists(key)).thenThrow(new RuntimeException("Test exception")); + + Exception exception = assertThrows(CityException.class, () -> cityService.getById(cityId)); + assertTrue(exception.getMessage().contains("ERROR :: cannot get city with id")); + } + + @Test + void testSave_Successful() throws Exception { + when(cityRepository.save(city)).thenReturn(city); + + City result = cityService.save(city); + + assertNotNull(result); + assertEquals(city, result); + } + + @Test + void testSave_ExceptionThrown() { + when(cityRepository.save(city)).thenThrow(new RuntimeException("Test exception")); + + Exception exception = assertThrows(CityException.class, () -> cityService.save(city)); + assertTrue(exception.getMessage().contains("ERROR :: cannot save city with id")); + } + + @Test + void testDelete_Successful() throws Exception { + int cityId = 1; + + doNothing().when(cityRepository).delete(cityId); + + assertDoesNotThrow(() -> cityService.delete(cityId)); + } + + @Test + void testDelete_ExceptionThrown() { + int cityId = 1; + + doThrow(new RuntimeException("Test exception")).when(cityRepository).delete(cityId); + + Exception exception = assertThrows(CityException.class, () -> cityService.delete(cityId)); + assertTrue(exception.getMessage().contains("ERROR :: cannot delete city with id")); + } +} \ No newline at end of file diff --git a/src/test/java/com/javarush/services/CountryServiceTest.java b/src/test/java/com/javarush/services/CountryServiceTest.java new file mode 100644 index 0000000..2d34758 --- /dev/null +++ b/src/test/java/com/javarush/services/CountryServiceTest.java @@ -0,0 +1,117 @@ +package com.javarush.services; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.javarush.DataTransformer; +import com.javarush.cache.RedisRepository; +import com.javarush.domain.entity.Country; +import com.javarush.domain.exceptions.CountryException; +import com.javarush.redis.CityCountry; +import com.javarush.repository.CountryRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +class CountryServiceTest { + @Mock + private RedisRepository redisRepository; + + @Mock + private CountryRepository countryRepository; + + @InjectMocks + private CountryService countryService; + + private Country country; + + @BeforeEach + void setUp() { + MockitoAnnotations.initMocks(this); + country = new Country(); + } + + @Test + void testGetById_CountryExistsInRedis() throws JsonProcessingException { + int id = 1; + String key = "country_" + id; + CityCountry testCityCountry = DataTransformer.countryTransformToCityCountry(country); + + when(redisRepository.checkExists(key)).thenReturn(true); + when(redisRepository.getById(key, CityCountry.class)).thenReturn(testCityCountry); + + Country country1 = countryService.getById(id); + + assertNotNull(country1); + assertEquals(country1, country); + verify(redisRepository, times(1)).getById(key, CityCountry.class); + } + + @Test + void testGetById_CountryNotInRedis() throws Exception { + int countryId = 1; + String key = "country_" + countryId; + + when(redisRepository.checkExists(key)).thenReturn(false); + when(countryRepository.getById(countryId)).thenReturn(country); + + Country result = countryService.getById(countryId); + + assertNotNull(result); + verify(redisRepository, times(1)).put(eq(key), any(CityCountry.class)); + assertEquals(country, result); + } + + @Test + void testGetById_ExceptionThrown() { + int countryId = 1; + String key = "country_" + countryId; + + when(redisRepository.checkExists(key)).thenThrow(new RuntimeException("Test exception")); + + Exception exception = assertThrows(CountryException.class, () -> countryService.getById(countryId)); + assertTrue(exception.getMessage().contains("ERROR :: cannot get country with id")); + } + + @Test + void testSave_Successful() throws Exception { + when(countryRepository.save(country)).thenReturn(country); + + Country result = countryService.save(country); + + assertNotNull(result); + assertEquals(country, result); + } + + @Test + void testSave_ExceptionThrown() { + when(countryRepository.save(country)).thenThrow(new RuntimeException("Test exception")); + + Exception exception = assertThrows(CountryException.class, () -> countryService.save(country)); + assertTrue(exception.getMessage().contains("ERROR :: could not save country with id")); + } + + @Test + void testDelete_Successful() throws Exception { + int countryId = 1; + + doNothing().when(countryRepository).delete(countryId); + + assertDoesNotThrow(() -> countryService.delete(countryId)); + } + + @Test + void testDelete_ExceptionThrown() { + int countryId = 1; + + doThrow(new RuntimeException("Test exception")).when(countryRepository).delete(countryId); + + Exception exception = assertThrows(CountryException.class, () -> countryService.delete(countryId)); + assertTrue(exception.getMessage().contains("ERROR :: cannot delete country with id")); + } +} \ No newline at end of file