Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 11 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
# Практическое задание 2 # Redis Map

В рамках практической задачи нужно реализовать прослойку между Redis и интерфейсом Map
Достаточно будет реализации Map<String, String>
## Краткое описание проделанной работы
- Реализован класс `org.redis.RedisMap` — обёртка над Redis Hash, соответствующая `Map<String, String>`, с использованием Jedis.
- Обеспечена безопасная работа с ресурсами (`try-with-resources`), добавлен `AutoCloseable` для корректного закрытия пула при владении им.
- Возвращаются немодифицируемые снимки `keySet`/`values`/`entrySet`; null-ключи и null-значения не поддерживаются.
- Покрыто модульными тестами JUnit 5; каждый тест использует уникальный `hashKey` для изоляции; тесты проходят успешно.

**Описание:**<br>
✅ Написать реализацию методов Map<String, String>, которая будет работать с Redis<br>
✅ Работать с redis можно через Jedis<br>
✅ Покрыть все методы тестами<br>
✅ Для поднятия redis, в корне проекта есть docker-compose файл <br>

Redis поднимается командой
## Как запустить проверки
1) Поднять Redis:
```
docker-compose up -d
```

**Как выполнить:**
1. Сделать форк этого проекта
2. Сделать ветку dev в своем проекте. Выполнять работу в ней
3. После выполнения работы открыть Pull Request в master
4. Назначить ментора ревьювером
5. Отправить ссылку на PR в тг чат ментору
2) Запустить тесты:
```
mvn test
```
344 changes: 344 additions & 0 deletions src/main/java/org/redis/RedisMap.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,344 @@
package org.redis;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

/**
* Реализация Map<String, String> поверх одного Redis Hash с использованием Jedis.
* Ключи соответствуют полям хеша, значения — значениям полей.
* <p>Допущения для реализации:
* <br>- null ключи и null значения не поддерживаются (бросается NullPointerException)
* <br>- Коллекционные представления (keySet/values/entrySet) возвращаются как немодифицируемые снимки
*/
public class RedisMap implements Map<String, String>, AutoCloseable {

public static final String KEY_MUST_NOT_BE_NULL = "Ключ не может быть null";
public static final String VALUE_MUST_NOT_BE_NULL = "Значение не может быть null";
public static final String COLLECTION_MUST_NOT_BE_NULL = "Коллекция не может быть null";

private final JedisPool pool;
private final String hashKey;
private final boolean ownPool;

private static final String DEFAULT_HOST = "localhost";
private static final int DEFAULT_PORT = 6379;
private static final String DEFAULT_HASH_KEY = "redis-map";

/**
* Создаёт карту, подключённую к Redis на localhost:6379 и использующую хеш "redis-map".
*/
public RedisMap() {
this(new JedisPool(DEFAULT_HOST, DEFAULT_PORT), DEFAULT_HASH_KEY, true);
}

/**
* Создаёт карту с указанными параметрами подключения и именем Redis-хеша.
*
* @param host хост Redis
* @param port порт Redis
* @param hashKey имя Redis-хеша для хранения данных
*/
public RedisMap(String host, int port, String hashKey) {
this(new JedisPool(Objects.requireNonNull(host, "host"), port), hashKey, true);
}

/**
* Создаёт карту поверх указанного пула соединений и Redis-хеша.
*
* @param pool пул соединений Jedis
* @param hashKey имя Redis-хеша для хранения данных
*/
public RedisMap(JedisPool pool, String hashKey) {
this(pool, hashKey, false);
}

/**
* Создаёт карту поверх переданного пула, с указанием владения пулом.
*
* @param pool пул соединений Jedis
* @param hashKey имя Redis-хеша для хранения данных
* @param ownPool признак, что карта владеет пулом и должна его закрыть
*/
private RedisMap(JedisPool pool, String hashKey, boolean ownPool) {
this.pool = Objects.requireNonNull(pool, "pool");
this.hashKey = Objects.requireNonNull(hashKey, "hashKey");
this.ownPool = ownPool;
}

/**
* Возвращает количество записей (HLEN).
*
* @return размер карты
*/
@Override
public int size() {
try (Jedis jedis = pool.getResource()) {
long length = jedis.hlen(hashKey);
return (int) Math.min(length, Integer.MAX_VALUE);
}
}

/**
* Проверяет, пуста ли карта (HLEN == 0).
*
* @return true, если карта пуста
*/
@Override
public boolean isEmpty() {
return size() == 0;
}

/**
* Проверяет наличие ключа (HEXISTS).
* null ключи не поддерживаются.
* Ключи не типа String считаются отсутствующими.
*
* @param key ключ
* @return true, если ключ присутствует
*/
@Override
public boolean containsKey(Object key) {
Objects.requireNonNull(key, KEY_MUST_NOT_BE_NULL);
if (!(key instanceof String)) {
return false;
}
try (Jedis jedis = pool.getResource()) {
return jedis.hexists(hashKey, (String) key);
}
}

/**
* Проверяет наличие значения путём перебора всех значений (HVALS).
* null значения не поддерживаются.
* Значения не типа String считаются отсутствующими.
*
* @param value значение
* @return true, если значение присутствует
*/
@Override
public boolean containsValue(Object value) {
Objects.requireNonNull(value, VALUE_MUST_NOT_BE_NULL);
if (!(value instanceof String)) {
return false;
}
try (Jedis jedis = pool.getResource()) {
List<String> valueList = jedis.hvals(hashKey);
for (String currentValue : valueList) {
if (Objects.equals(currentValue, value)) {
return true;
}
}
return false;
}
}

/**
* Возвращает значение по ключу (HGET).
* null ключи не поддерживаются.
* Для ключей не типа String возвращает null.
*
* @param key ключ
* @return значение или null, если отсутствует
*/
@Override
public String get(Object key) {
Objects.requireNonNull(key, KEY_MUST_NOT_BE_NULL);
if (!(key instanceof String)) {
return null;
}
try (Jedis jedis = pool.getResource()) {
return jedis.hget(hashKey, (String) key);
}
}

/**
* Записывает значение по ключу (HSET), возвращая предыдущее (HGET).
* null ключи и значения не поддерживаются.
*
* @param key ключ
* @param value значение
* @return предыдущее значение или null, если не было
*/
@Override
public String put(String key, String value) {
Objects.requireNonNull(key, KEY_MUST_NOT_BE_NULL);
Objects.requireNonNull(value, VALUE_MUST_NOT_BE_NULL);
try (Jedis jedis = pool.getResource()) {
String previousValue = jedis.hget(hashKey, key);
jedis.hset(hashKey, key, value);
return previousValue;
}
}

/**
* Удаляет значение по ключу (HDEL), возвращая предыдущее (HGET).
* null ключи не поддерживаются.
* Ключи не типа String игнорируются (возвращается null).
*
* @param key ключ
* @return предыдущее значение или null
*/
@Override
public String remove(Object key) {
Objects.requireNonNull(key, KEY_MUST_NOT_BE_NULL);
if (!(key instanceof String)) {
return null;
}
try (Jedis jedis = pool.getResource()) {
String previousValue = jedis.hget(hashKey, (String) key);
if (previousValue != null) {
jedis.hdel(hashKey, (String) key);
}
return previousValue;
}
}

/**
* Массовая запись полей (HSET с картой).
* null ключи/значения не поддерживаются.
*
* @param m карта для записи
*/
@Override
public void putAll(Map<? extends String, ? extends String> m) {
Objects.requireNonNull(m, COLLECTION_MUST_NOT_BE_NULL);
if (m.isEmpty()) return;

Map<String, String> copy = new HashMap<>(m.size());
for (Entry<? extends String, ? extends String> entry : m.entrySet()) {
String key = Objects.requireNonNull(entry.getKey(), KEY_MUST_NOT_BE_NULL);
String valueToPut = Objects.requireNonNull(entry.getValue(), VALUE_MUST_NOT_BE_NULL);
copy.put(key, valueToPut);
}

try (Jedis jedis = pool.getResource()) {
jedis.hset(hashKey, copy);
}
}

/**
* Полная очистка карты (DEL хеша).
*/
@Override
public void clear() {
try (Jedis jedis = pool.getResource()) {
jedis.del(hashKey);
}
}

/**
* Возвращает снимок множества ключей (HKEYS).
*
* @return снимок множества ключей
*/
@Override
public Set<String> keySet() {
try (Jedis jedis = pool.getResource()) {
Set<String> keys = jedis.hkeys(hashKey);
return Set.copyOf(keys);
}
}

/**
* Возвращает снимок коллекции значений (HVALS).
*
* @return снимок коллекции значений
*/
@Override
public Collection<String> values() {
try (Jedis jedis = pool.getResource()) {
List<String> valueList = jedis.hvals(hashKey);
return List.copyOf(valueList);
}
}

/**
* Возвращает снимок множества пар (HGETALL).
*
* @return снимок множества пар
*/
@Override
public Set<Entry<String, String>> entrySet() {
try (Jedis jedis = pool.getResource()) {
Map<String, String> allEntries = jedis.hgetAll(hashKey);
Set<Entry<String, String>> entriesSnapshot = new HashSet<>(allEntries.size());
for (Entry<String, String> entry : allEntries.entrySet()) {
entriesSnapshot.add(new SimpleImmutableEntry<>(entry));
}
return Collections.unmodifiableSet(entriesSnapshot);
}
}

/**
* Сравнение по содержимому как у обычной Map (по снимку данных).
*
* @param o другой объект
* @return true, если содержимое эквивалентно
*/
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Map<?, ?> other)) return false;
Map<String, String> snapshotMap = snapshot();
return snapshotMap.equals(other);
}

/**
* Хэш-код по контракту Map на основе снимка данных.
*
* @return хэш-код
*/
@Override
public int hashCode() {
return snapshot().hashCode();
}

/**
* Возвращает снимок данных как обычную HashMap.
*
* @return снимок карты
*/
private Map<String, String> snapshot() {
try (Jedis jedis = pool.getResource()) {
return jedis.hgetAll(hashKey);
}
}

/**
* Закрывает пул соединений, если пул создан и управляется этой картой.
*/
@Override
public void close() {
if (ownPool) {
pool.close();
}
}

/**
* Возвращает имя Redis-хеша, используемого этой картой.
*
* @return имя хеша
*/
public String getHashKey() {
return hashKey;
}

/**
* Возвращает пул соединений Jedis, используемый картой.
*
* @return пул Jedis
*/
public JedisPool getPool() {
return pool;
}
}
Loading