
io.fluxcapacitor.common.api.Metadata Maven / Gradle / Ivy
Show all versions of common Show documentation
/*
* Copyright (c) Flux Capacitor IP B.V. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.fluxcapacitor.common.api;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.json.JsonMapper;
import io.fluxcapacitor.common.serialization.NullCollectionsAsEmptyModule;
import lombok.NonNull;
import lombok.SneakyThrows;
import lombok.Value;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
import static com.fasterxml.jackson.databind.SerializationFeature.FAIL_ON_EMPTY_BEANS;
import static java.lang.String.format;
import static java.util.Collections.emptyMap;
import static java.util.stream.Collectors.toMap;
@Value
public class Metadata {
public static JsonMapper objectMapper = JsonMapper.builder()
.findAndAddModules().addModule(new NullCollectionsAsEmptyModule())
.disable(FAIL_ON_EMPTY_BEANS).disable(FAIL_ON_UNKNOWN_PROPERTIES)
.build();
Map entries;
@JsonAnyGetter
public Map getEntries() {
return entries;
}
public static Metadata of(Object... keyValues) {
return Metadata.empty().with(keyValues);
}
public static Metadata empty() {
return new Metadata(emptyMap());
}
public static Metadata of(Object key, Object value) {
return Metadata.empty().with(key, value);
}
public static Metadata of(Map, ?> map) {
return Metadata.empty().with(map);
}
@JsonCreator
private Metadata(Map entries) {
this.entries = entries;
}
@Override
public String toString() {
return entries.toString();
}
/*
Add
*/
public Metadata with(Map, ?> values) {
Map map = new HashMap<>(entries);
values.forEach((key, value) -> with(key, value, map));
return new Metadata(map);
}
public Metadata with(Metadata metadata) {
Map map = new HashMap<>(entries);
map.putAll(metadata.entries);
return new Metadata(map);
}
public Metadata with(Object... keyValues) {
if (keyValues.length % 2 == 1) {
throw new IllegalArgumentException("Failed to create metadata for keys " + Arrays.toString(keyValues));
}
Map map = new HashMap<>(entries);
for (int i = 0; i < keyValues.length; i += 2) {
with(keyValues[i].toString(), keyValues[i + 1], map);
}
return new Metadata(map);
}
@SneakyThrows
public Metadata with(Object key, Object value) {
return new Metadata(with(key, value, new HashMap<>(entries)));
}
public Metadata addIfAbsent(Object key, Object value) {
return containsKey(key) ? this : with(key, value);
}
@SneakyThrows
private static Map with(@NonNull Object key, Object value, Map entries) {
String keyString = key.toString();
if (value == null) {
entries.remove(keyString);
return entries;
}
if (value instanceof Optional> optional) {
if (optional.isEmpty()) {
return entries;
}
value = optional.get();
}
entries.put(keyString, value instanceof String ? (String) value : objectMapper.writeValueAsString(value));
return entries;
}
/**
* Adds your custom trace information to the metadata.
* Trace metadata is passed down from message to message, similar to $traceId.
* When message A is caused by message B, all trace metadata is copied from B to A.
* If message C is caused by B, again all traces are copied.
* You end up with a trace of all messages indirectly caused by your message.
*
* Trace metadata is prefixed with "$trace.", and the CorrelatingInterceptor copies it from message to message.
*/
@SneakyThrows
private static Map withTrace(Object key, Object value, Map entries) {
return with("$trace." + key, value, entries);
}
@SneakyThrows
public Metadata withTrace(Object key, Object value) {
return new Metadata(withTrace(key, value, new HashMap<>(entries)));
}
/*
Remove
*/
public Metadata without(Object key) {
Map map = new HashMap<>(entries);
map.remove(key.toString());
return new Metadata(map);
}
public Metadata withoutIf(Predicate check) {
Map map = new HashMap<>(entries);
Iterator iterator = map.keySet().iterator();
iterator.forEachRemaining(key -> {
if (check.test(key)) {
iterator.remove();
}
});
return new Metadata(map);
}
/*
Query
*/
public String get(Object key) {
return entries.get(key.toString());
}
@SuppressWarnings("unchecked")
@SneakyThrows
public T get(Object key, Class type) {
String value = get(key);
if (value == null) {
return null;
}
if (String.class.isAssignableFrom(type)) {
return (T) value;
}
try {
return objectMapper.readValue(value, type);
} catch (IOException e) {
throw new IllegalStateException(format("Failed to deserialize value %s to a %s for key %s",
value, type.getSimpleName(), key), e);
}
}
@SneakyThrows
public T get(Object key, TypeReference type) {
String value = get(key);
if (value == null) {
return null;
}
try {
return objectMapper.readValue(value, type);
} catch (IOException e) {
throw new IllegalStateException(format("Failed to deserialize value %s to a %s for key %s",
value, type, key), e);
}
}
@JsonIgnore
public Map getTraceEntries() {
return entrySet().stream().filter(e -> e.getKey().startsWith("$trace."))
.collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
}
public boolean containsKey(Object key) {
return entries.containsKey(key.toString());
}
public boolean containsAnyKey(Object... keys) {
return Arrays.stream(keys).anyMatch(this::containsKey);
}
public boolean contains(@NonNull Object key, @NonNull Object value) {
Object result = value instanceof String ? get(key) : get(key, value.getClass());
return Objects.equals(result, value);
}
public boolean contains(@NonNull Metadata metadata) {
return entries.entrySet().containsAll(metadata.entries.entrySet());
}
public String getOrDefault(Object key, String defaultValue) {
return entries.getOrDefault(key.toString(), defaultValue);
}
public Set> entrySet() {
return entries.entrySet();
}
}