package com.linkedin.thirdeye.api;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.io.Serializable;
import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.ObjectUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Stores key-value pairs of dimension name and value. The paris are sorted by their dimension names in ascending order.
*
* To reduces the length of string to be stored in database, this class implements SortedMap<String, String> for
* converting to/from Json string in Map format, i.e., instead of storing {"sortedDimensionMap":{"country":"US",
* "page_name":"front_page"}}, we only need to store {"country":"US","page_name":"front_page"}.
*/
public class DimensionMap implements SortedMap<String, String>, Comparable<DimensionMap>, Serializable {
private static final Logger LOG = LoggerFactory.getLogger(DimensionMap.class);
private static ObjectMapper OBJECT_MAPPER = new ObjectMapper();
// Dimension name to dimension value pairs, which are sorted by dimension names
private SortedMap<String, String> sortedDimensionMap = new TreeMap<>();
/**
* Constructs an empty dimension map.
*/
public DimensionMap() {
}
/**
* Constructs a dimension map from a json string; if the given string is not in Json format, then this method
* falls back to parse Java's map string format, which is {key1=value1,key2=value2}.
*
* @param value the json string that represents this dimension map.
*/
public DimensionMap(String value) {
try {
sortedDimensionMap = OBJECT_MAPPER.readValue(value, TreeMap.class);
} catch (IOException e) {
try { // Fall back to Java's map string, which is {key=value}.
value = value.substring(1, value.length() - 1); // Remove curly brackets
String[] keyValuePairs = value.split(",");
for (String pair : keyValuePairs) {
String[] entry = pair.split("=");
sortedDimensionMap.put(entry[0].trim(), entry[1].trim());
}
} catch (Exception finalE) {
LOG.error("Failed to initialize dimension map from this string: {}", value);
}
}
}
/**
* Returns a dimension map according to the given dimension key.
*
* Assume that the given dimension key is [US,front_page,*,*,...] and the schema dimension names are
* [country,page_name,...], then this method return this dimension map: {country=US; page_name=front_page;}
*
* @param dimensionKey the dimension key to be used to covert to explored dimensions
* @param schemaDimensionNames the schema dimension names
* @return the key-value pair of dimension value and dimension name according to the given dimension key.
*/
public static DimensionMap fromDimensionKey(DimensionKey dimensionKey, List<String> schemaDimensionNames) {
DimensionMap dimensionMap = new DimensionMap();
if (CollectionUtils.isNotEmpty(schemaDimensionNames)) {
String[] dimensionValues = dimensionKey.getDimensionValues();
for (int i = 0; i < dimensionValues.length; ++i) {
String dimensionValue = dimensionValues[i].trim();
if (!dimensionValue.equals("") && !dimensionValue.equals("*")) {
String dimensionName = schemaDimensionNames.get(i);
dimensionMap.put(dimensionName, dimensionValue);
}
}
}
return dimensionMap;
}
/**
* Returns if this dimension map equals or is a child of the given dimension map, i.e., the given dimension map is
* a subset of this dimension map.
*
* @param that the given dimension map.
* @return true if this dimension map is a child of the given dimension map.
*/
public boolean equalsOrChildOf(DimensionMap that) {
// A null dimension map equals to an empty dimension map, which is the root level of all dimensions
if (that == null) {
return true;
} else if (that.size() < this.size()) {
// parent dimension map must be a subset of this dimension map
for (Entry<String, String> parentDimensionEntry : that.entrySet()) {
String thisDimensionValue = this.get(parentDimensionEntry.getKey());
if (!parentDimensionEntry.getValue().equals(thisDimensionValue)) {
return false;
}
}
return true;
} else if (that.size() == this.size()) {
return this.equals(that);
} else {
return false;
}
}
/**
* Returns Java's string representation for Map class, which is in the form of {key1=value1,key2=value2}.
* @return Java's string representation for Map class.
*/
public String toJavaString() {
return sortedDimensionMap.toString();
}
/**
* Returns Json string representation for Map class, which is in the form of {"key1":"value1","key2"="value2"}.
* @return Json string representation for Map class.
* @throws JsonProcessingException
*/
public String toJson() throws JsonProcessingException {
return OBJECT_MAPPER.writeValueAsString(this);
}
/**
* Returns a JSON string representation of this dimension map for {@link com.linkedin.thirdeye.datalayer.dao.GenericPojoDao}
* to persistent the map to backend database.
*
* It returns the generic string representation of this dimension map if any exception occurs when generating the JSON
* string. In that case, the constructor {@link DimensionMap(String)} will be invoked during the construction of that
* dimension map.
*
* @return a JSON string representation of this dimension map for {@link com.linkedin.thirdeye.datalayer.dao.GenericPojoDao}
* to persistent the map to backend database.
*/
@Override
public String toString() {
try {
return this.toJson();
} catch (JsonProcessingException e) {
return super.toString();
}
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
DimensionMap that = (DimensionMap) o;
return Objects.equals(sortedDimensionMap, that.sortedDimensionMap);
}
@Override
public int hashCode() {
return Objects.hash(sortedDimensionMap);
}
/**
* Returns the compared result of the string representation of dimension maps.
*
* Examples:
* 1. a={}, b={"K"="V"} --> a="", b="KV" --> a < b
* 2. a={"K"="V"}, b={"K"="V"} --> a="KV", b="KV" --> a = b
* 3. a={"K2"="V1"}, b={"K1"="V1","K2"="V2"} --> a="K2V1", b="K1V1K2V2" --> a > b
*
* @param o the dimension to compare to
*/
@Override
public int compareTo(DimensionMap o) {
Iterator<Map.Entry<String, String>> thisIte = sortedDimensionMap.entrySet().iterator();
Iterator<Map.Entry<String, String>> thatIte = o.sortedDimensionMap.entrySet().iterator();
while (thisIte.hasNext()) {
// o is a smaller map
if (!thatIte.hasNext()) {
return 1;
}
Map.Entry<String, String> thisEntry = thisIte.next();
Map.Entry<String, String> thatEntry = thatIte.next();
// Compare dimension name first
int diff = ObjectUtils.compare(thisEntry.getKey(), thatEntry.getKey());
if (diff != 0) {
return diff;
}
// Compare dimension value afterwards
diff = ObjectUtils.compare(thisEntry.getValue(), thatEntry.getValue());
if (diff != 0) {
return diff;
}
}
// o is a larger map
if (thatIte.hasNext()) {
return -1;
}
return 0;
}
@Override
public Comparator<? super String> comparator() {
return sortedDimensionMap.comparator();
}
@Override
public SortedMap<String, String> subMap(String fromKey, String toKey) {
return sortedDimensionMap.subMap(fromKey, toKey);
}
@Override
public SortedMap<String, String> headMap(String toKey) {
return sortedDimensionMap.headMap(toKey);
}
@Override
public SortedMap<String, String> tailMap(String fromKey) {
return sortedDimensionMap.tailMap(fromKey);
}
@Override
public String firstKey() {
return sortedDimensionMap.firstKey();
}
@Override
public String lastKey() {
return sortedDimensionMap.lastKey();
}
@Override
public int size() {
return sortedDimensionMap.size();
}
@Override
public boolean isEmpty() {
return sortedDimensionMap.isEmpty();
}
@Override
public boolean containsKey(Object key) {
return sortedDimensionMap.containsKey(key);
}
@Override
public boolean containsValue(Object value) {
return sortedDimensionMap.containsValue(value);
}
@Override
public String get(Object key) {
return sortedDimensionMap.get(key);
}
@Override
public String put(String key, String value) {
return sortedDimensionMap.put(key, value);
}
@Override
public String remove(Object key) {
return sortedDimensionMap.remove(key);
}
@Override
public void putAll(Map<? extends String, ? extends String> m) {
sortedDimensionMap.putAll(m);
}
@Override
public void clear() {
sortedDimensionMap.clear();
}
@Override
public Set<String> keySet() {
return sortedDimensionMap.keySet();
}
@Override
public Collection<String> values() {
return sortedDimensionMap.values();
}
@Override
public Set<Entry<String, String>> entrySet() {
return sortedDimensionMap.entrySet();
}
}