package com.bugsnag.android;
import android.support.annotation.NonNull;
import java.io.IOException;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Observable;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* A container for additional diagnostic information you'd like to send with
* every error report.
*
* Diagnostic information is presented on your Bugsnag dashboard in tabs.
*/
public class MetaData extends Observable implements JsonStream.Streamable {
private static final String FILTERED_PLACEHOLDER = "[FILTERED]";
private static final String OBJECT_PLACEHOLDER = "[OBJECT]";
private String[] filters;
final Map<String, Object> store;
/**
* Create an empty MetaData object.
*/
public MetaData() {
store = new ConcurrentHashMap<String, Object>();
}
/**
* Create a MetaData with values copied from an existing Map
*/
public MetaData(Map<String, Object> m) {
store = new ConcurrentHashMap<String, Object>(m);
}
public void toStream(@NonNull JsonStream writer) throws IOException {
objectToStream(store, writer);
}
/**
* Add diagnostic information to a tab of this MetaData.
*
* For example:
*
* metaData.addToTab("account", "name", "Acme Co.");
* metaData.addToTab("account", "payingCustomer", true);
*
* @param tabName the dashboard tab to add diagnostic data to
* @param key the name of the diagnostic information
* @param value the contents of the diagnostic information
*/
public void addToTab(String tabName, String key, Object value) {
addToTab(tabName, key, value, true);
}
/**
* Add diagnostic information to a tab of this MetaData.
*
* For example:
*
* metaData.addToTab("account", "name", "Acme Co.");
* metaData.addToTab("account", "payingCustomer", true);
*
* @param tabName the dashboard tab to add diagnostic data to
* @param key the name of the diagnostic information
* @param value the contents of the diagnostic information
* @param notify whether or not to notify any NDK observers about this change
*/
void addToTab(String tabName, String key, Object value, boolean notify) {
Map<String, Object> tab = getTab(tabName);
if(value != null) {
tab.put(key, value);
} else {
tab.remove(key);
}
notifyBugsnagObservers(NotifyType.META);
}
/**
* Remove a tab of diagnostic information from this MetaData.
*
* @param tabName the dashboard tab to remove diagnostic data from
*/
public void clearTab(String tabName) {
store.remove(tabName);
notifyBugsnagObservers(NotifyType.META);
}
Map<String, Object> getTab(String tabName) {
Map<String, Object> tab = (Map<String, Object>)store.get(tabName);
if(tab == null) {
tab = new ConcurrentHashMap<String, Object>();
store.put(tabName, tab);
}
return tab;
}
void setFilters(String... filters) {
this.filters = filters;
notifyBugsnagObservers(NotifyType.FILTERS);
}
static MetaData merge(MetaData... metaDataList) {
ArrayList<Map<String, Object>> stores = new ArrayList<Map<String, Object>>();
List<String> filters = new ArrayList<>();
for(MetaData metaData : metaDataList) {
if(metaData != null) {
stores.add(metaData.store);
if (metaData.filters != null) {
filters.addAll(Arrays.asList(metaData.filters));
}
}
}
MetaData newMeta = new MetaData(mergeMaps(stores.toArray(new Map[0])));
newMeta.filters = filters.toArray(new String[filters.size()]);
return newMeta;
}
private static Map<String, Object> mergeMaps(Map<String, Object>... maps) {
Map<String, Object> result = new ConcurrentHashMap<String, Object>();
for(Map<String, Object> map : maps) {
if(map == null) continue;
// Get a set of all possible keys in base and overrides
Set<String> allKeys = new HashSet<String>(result.keySet());
allKeys.addAll(map.keySet());
for(String key : allKeys) {
Object baseValue = result.get(key);
Object overridesValue = map.get(key);
if(overridesValue != null) {
if(baseValue != null && baseValue instanceof Map && overridesValue instanceof Map) {
// Both original and overrides are Maps, go deeper
result.put(key, mergeMaps((Map<String, Object>)baseValue, (Map<String, Object>)overridesValue));
} else {
result.put(key, overridesValue);
}
} else {
// No collision, just use base value
result.put(key, baseValue);
}
}
}
return result;
}
// Write complex/nested values to a JsonStreamer
private void objectToStream(Object obj, JsonStream writer) throws IOException {
if(obj == null) {
writer.nullValue();
} else if(obj instanceof String) {
writer.value((String)obj);
} else if(obj instanceof Number) {
writer.value((Number)obj);
} else if(obj instanceof Boolean) {
writer.value((Boolean)obj);
} else if(obj instanceof Map) {
// Map objects
writer.beginObject();
for(Iterator entries = ((Map)obj).entrySet().iterator(); entries.hasNext();) {
Map.Entry entry = (Map.Entry)entries.next();
Object keyObj = entry.getKey();
if(keyObj instanceof String) {
String key = (String)keyObj;
writer.name(key);
if(shouldFilter(key)) {
writer.value(FILTERED_PLACEHOLDER);
} else {
objectToStream(entry.getValue(), writer);
}
}
}
writer.endObject();
} else if(obj instanceof Collection) {
// Collection objects (Lists, Sets etc)
writer.beginArray();
for(Object entry : (Collection)obj) {
objectToStream(entry, writer);
}
writer.endArray();
} else if(obj.getClass().isArray()) {
// Primitive array objects
writer.beginArray();
int length = Array.getLength(obj);
for (int i = 0; i < length; i += 1) {
objectToStream(Array.get(obj, i), writer);
}
writer.endArray();
} else {
writer.value(OBJECT_PLACEHOLDER);
}
}
// Should this key be filtered
private boolean shouldFilter(String key) {
if(filters == null || key == null) return false;
for(String filter : filters) {
if(key.contains(filter)) {
return true;
}
}
return false;
}
private void notifyBugsnagObservers(NotifyType type) {
setChanged();
super.notifyObservers(type.getValue());
}
}