All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.redis.spring.batch.common.KeyComparisonItemReader Maven / Gradle / Ivy

The newest version!
package com.redis.spring.batch.common;

import java.time.Duration;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.springframework.batch.item.Chunk;
import org.springframework.util.CollectionUtils;

import com.redis.spring.batch.RedisItemReader;
import com.redis.spring.batch.common.KeyComparison.Status;
import com.redis.spring.batch.reader.AbstractKeyValueItemReader;
import com.redis.spring.batch.util.CodecUtils;

import io.lettuce.core.StreamMessage;

public class KeyComparisonItemReader extends RedisItemReader {

	public static final Duration DEFAULT_TTL_TOLERANCE = Duration.ofMillis(100);

	private final AbstractKeyValueItemReader source;
	private final AbstractKeyValueItemReader target;

	private Duration ttlTolerance = DEFAULT_TTL_TOLERANCE;

	private ValueReader> sourceValueReader;
	private ValueReader> targetValueReader;
	private Function, KeyValue> processor = Function.identity();
	private boolean compareStreamMessageIds;

	public KeyComparisonItemReader(AbstractKeyValueItemReader source,
			AbstractKeyValueItemReader target) {
		super(source.getClient(), CodecUtils.STRING_CODEC);
		this.source = source;
		this.target = target;
	}

	public void setCompareStreamMessageIds(boolean enable) {
		this.compareStreamMessageIds = enable;
	}

	public void setProcessor(Function, KeyValue> processor) {
		this.processor = processor;
	}

	public void setTtlTolerance(Duration ttlTolerance) {
		this.ttlTolerance = ttlTolerance;
	}

	@Override
	protected synchronized void doOpen() throws Exception {
		if (sourceValueReader == null) {
			sourceValueReader = source.valueReader();
			sourceValueReader.open();
		}
		if (targetValueReader == null) {
			targetValueReader = target.valueReader();
			targetValueReader.open();
		}
		super.doOpen();
	}

	@Override
	protected synchronized void doClose() throws Exception {
		super.doClose();
		if (sourceValueReader != null) {
			sourceValueReader.close();
			sourceValueReader = null;
		}
		if (targetValueReader != null) {
			targetValueReader.close();
			targetValueReader = null;
		}
	}

	@SuppressWarnings("unchecked")
	@Override
	public Chunk values(Chunk keys) {
		Chunk comparisons = new Chunk<>();
		Chunk processedKeys = processKeys((Chunk) keys);
		Chunk> sourceItems = sourceValueReader.execute(processedKeys);
		List> items = processValues(sourceItems).getItems();
		List targetKeys = items.stream().map(KeyValue::getKey).collect(Collectors.toList());
		List> targetItems = targetValueReader.execute(new Chunk<>(targetKeys)).getItems();
		for (int index = 0; index < items.size(); index++) {
			KeyComparison comparison = new KeyComparison();
			comparison.setSource(items.get(index));
			if (index < targetItems.size()) {
				comparison.setTarget(targetItems.get(index));
			}
			comparison.setStatus(status(comparison));
			comparisons.add(comparison);
		}
		return comparisons;
	}

	private Chunk> processValues(Chunk> values) {
		if (processor == null) {
			return values;
		}
		Chunk> processedValues = new Chunk<>();
		for (KeyValue value : values) {
			KeyValue processedValue = processor.apply(value);
			if (processedValue != null) {
				processedValues.add(processedValue);
			}
		}
		return processedValues;
	}

	private Chunk processKeys(Chunk keys) {
		if (keyProcessor == null) {
			return keys;
		}
		Chunk processedKeys = new Chunk<>();
		for (String key : keys) {
			try {
				String processedKey = keyProcessor.process(key);
				if (processedKey != null) {
					processedKeys.add(processedKey);
				}
			} catch (Exception e) {
				// ignore
			}
		}
		return processedKeys;
	}

	private Status status(KeyComparison comparison) {
		KeyValue sourceEntry = comparison.getSource();
		KeyValue targetEntry = comparison.getTarget();
		if (targetEntry == null) {
			if (sourceEntry == null) {
				return Status.OK;
			}
			return Status.MISSING;
		}
		if (!targetEntry.exists() && sourceEntry.exists()) {
			return Status.MISSING;
		}
		if (targetEntry.getType() != sourceEntry.getType()) {
			return Status.TYPE;
		}
		if (!valueEquals(sourceEntry, targetEntry)) {
			return Status.VALUE;
		}
		if (sourceEntry.getTtl() != targetEntry.getTtl()) {
			long delta = Math.abs(sourceEntry.getTtl() - targetEntry.getTtl());
			if (delta > ttlTolerance.toMillis()) {
				return Status.TTL;
			}
		}
		return Status.OK;
	}

	@SuppressWarnings({ "unchecked", "rawtypes" })
	private boolean valueEquals(KeyValue source, KeyValue target) {
		if (source.getType() == DataType.STREAM) {
			return streamEquals((Collection) source.getValue(),
					(Collection) target.getValue());
		}
		return Objects.deepEquals(source.getValue(), target.getValue());
	}

	@SuppressWarnings("rawtypes")
	private boolean streamEquals(Collection source, Collection target) {
		if (CollectionUtils.isEmpty(source)) {
			return CollectionUtils.isEmpty(target);
		}
		if (source.size() != target.size()) {
			return false;
		}
		Iterator sourceIterator = source.iterator();
		Iterator targetIterator = target.iterator();
		while (sourceIterator.hasNext()) {
			if (!targetIterator.hasNext()) {
				return false;
			}
			StreamMessage sourceMessage = sourceIterator.next();
			StreamMessage targetMessage = targetIterator.next();
			if (!streamMessageEquals(sourceMessage, targetMessage)) {
				return false;
			}
		}
		return true;
	}

	@SuppressWarnings("rawtypes")
	private boolean streamMessageEquals(StreamMessage sourceMessage, StreamMessage targetMessage) {
		if (!Objects.equals(sourceMessage.getStream(), targetMessage.getStream())) {
			return false;
		}
		if (compareStreamMessageIds && !Objects.equals(sourceMessage.getId(), targetMessage.getId())) {
			return false;
		}
		Map sourceBody = sourceMessage.getBody();
		Map targetBody = targetMessage.getBody();
		if (CollectionUtils.isEmpty(sourceBody)) {
			return CollectionUtils.isEmpty(targetBody);
		}
		return sourceBody.equals(targetBody);
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy