PatchableMapWrapper.java
package net.hostsharing.hsadminng.mapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import lombok.SneakyThrows;
import org.apache.commons.lang3.tuple.ImmutablePair;
import jakarta.validation.constraints.NotNull;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
import static java.util.Optional.ofNullable;
/** This class wraps another (usually persistent) map and
* supports applying `PatchMap` as well as a toString method with stable entry order.
*/
public class PatchableMapWrapper<T> implements Map<String, T> {
private static final ObjectMapper jsonWriter = new ObjectMapper()
.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true)
.configure(SerializationFeature.INDENT_OUTPUT, true);
private final Map<String, T> delegate;
private final Set<String> patched = new HashSet<>();
private PatchableMapWrapper(final Map<String, T> map) {
delegate = map;
}
public static <T> PatchableMapWrapper<T> of(final PatchableMapWrapper<T> currentWrapper, final Consumer<PatchableMapWrapper<T>> setWrapper, final Map<String, T> target) {
return ofNullable(currentWrapper).orElseGet(() -> {
final var newWrapper = new PatchableMapWrapper<T>(target);
setWrapper.accept(newWrapper);
return newWrapper;
});
}
public static <T> PatchableMapWrapper<T> of(final Map<String, T> delegate) {
return new PatchableMapWrapper<T>(delegate);
}
@NotNull
public static <E> ImmutablePair<String, E> entry(final String key, final E value) {
return new ImmutablePair<>(key, value);
}
public void assign(final Map<String, T> entries) {
if (entries != null ) {
delegate.clear();
delegate.putAll(entries);
patched.clear();
}
}
public void patch(final Map<String, T> patch) {
patch.forEach((key, value) -> {
if (value == null) {
remove(key);
} else {
put(key, value);
}
});
}
public boolean isPatched(final String propertyName) {
return patched.contains(propertyName);
}
@SneakyThrows
public String toString() {
return jsonWriter.writeValueAsString(delegate);
}
// --- below just delegating methods --------------------------------
@Override
public int size() {
return delegate.size();
}
@Override
public boolean isEmpty() {
return delegate.isEmpty();
}
@Override
public boolean containsKey(final Object key) {
return delegate.containsKey(key);
}
@Override
public boolean containsValue(final Object value) {
return delegate.containsValue(value);
}
@Override
public T get(final Object key) {
return delegate.get(key);
}
@Override
public T put(final String key, final T value) {
if (!Objects.equals(value, delegate.get(key))) {
patched.add(key);
}
return delegate.put(key, fixValueType(value));
}
@Override
public T remove(final Object key) {
if (delegate.containsKey(key.toString())) {
patched.add(key.toString());
}
return delegate.remove(key);
}
@Override
public void putAll(final @NotNull Map<? extends String, ? extends T> m) {
delegate.putAll(m);
}
@Override
public void clear() {
patched.addAll(delegate.keySet());
delegate.clear();
}
@Override
@NotNull
public Set<String> keySet() {
return delegate.keySet();
}
@Override
@NotNull
public Collection<T> values() {
return delegate.values();
}
@Override
@NotNull
public Set<Entry<String, T>> entrySet() {
return delegate.entrySet();
}
private T fixValueType(final T value) {
if (value instanceof ArrayList<?> arrayListValue) {
// Jackson deserialization creates ArrayList for JSON arrays, but we need a String[].
// Jackson could be configured to create Object[], but that does not help.
final var valueToPut = arrayListValue.stream().map(Object::toString).toArray(String[]::new);
//noinspection unchecked
return (T) valueToPut;
}
return value;
}
}