/**
* @(#) KeenUtils.java
*
* This file is part of the Course Scheduler, an open source, cross platform
* course scheduling tool, configurable for most universities.
*
* Copyright (C) 2010-2014 Devyse.io; All rights reserved.
*
* @license GNU General Public License version 3 (GPLv3)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package io.devyse.scheduler.analytics.keen;
import io.devyse.scheduler.security.Privacy;
import io.keen.client.java.KeenLogging;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import org.slf4j.ext.XLogger;
import org.slf4j.ext.XLoggerFactory;
/**
* Keen utility methods for building events and mapping data to events
*
* @author Mike Reinhold
* @since 4.12.5
*/
class KeenUtils {
/**
* Static logger
*/
private static final XLogger logger = XLoggerFactory.getXLogger(KeenUtils.class);
/**
* Keen IO internally uses JSON to represent events. The Keen IO Java API requires that a
* nested map, essentially identical in structure to a JSON document, is sent as an event.
* Since flat maps are not allowed, the map keys within each level cannot include ".".
*
* Value: {@value}
*/
protected static final String MAP_SCOPING_DELIMITER = ".";
/**
* The key used to represent the value of an entry at the current scope or level. This is
* modeled after DNS usage of "@" as the entry for the current domain or scope.
*
* This is used automatically by the nested map methods if there are other values at the
* same level in the nested map.
*
* Value: {@value}
*/
protected static final String MAP_LEVEL_VALUE_KEY = "@";
/**
* Convert the specified flat map into a nested map for use in a Keen event.
*
* @param flatMap the flat map of event data
*
* @return a nested map containing the same event data as the flat map
*/
protected static Map<String, Object> createNestedMap(Map<String, Object> flatMap){
Map<String, Object> nestedMap = new HashMap<String, Object>();
for(Entry<String, Object> entry: flatMap.entrySet()){
Object value = entry.getValue();
try{
//if the value at this key is a map, we need to nest that submap as well
@SuppressWarnings("unchecked") //catch cast exception below
Map<String, Object> subMap = (Map<String, Object>)value;
value = createNestedMap(subMap);
}catch(ClassCastException e){}
addNestedMapEntry(nestedMap, entry.getKey(), value);
}
return nestedMap;
}
/**
* Add the value into the nested map via the key. A nested map is similar to a JSON
* document.
*
* @param map the nested map in which to insert the value at the key
* @param key the key specifying the location in the nested map in which to insert the value
* @param value the object to insert into the nested map
*/
protected static void addNestedMapEntry(Map<String, Object> map, String key, Object value){
if(key.contains(MAP_SCOPING_DELIMITER)){ //check if the key contains multiple levels
int left = key.indexOf(MAP_SCOPING_DELIMITER);
String parent = key.substring(0, left);
String child = key.substring(left+1, key.length());
nestMapEntry(map, parent, child, value);
}else{
redactAndAddMapEntry(map, key, value);
}
}
/**
* Add the value into the nested map under the parent level using the specified child key.
*
* @param map the nested map containing the parent scope directly
* @param parent the key for the parent scope
* @param child the key into the parent scope
* @param value the object to insert into the nested map
*/
protected static void nestMapEntry(Map<String, Object> map, String parent, String child, Object value){
try{
//check if the value associated with the parent scope is a map
@SuppressWarnings("unchecked") //ok to suppress, exception caught and handled below
Map<String, Object> subMap = (Map<String,Object>)map.get(parent);
//if it doesn't exist, create it and insert it as the parent
if(subMap == null){
subMap = new HashMap<String, Object>();
map.put(parent, subMap);
}
//add the value into the submap using the child key
addNestedMapEntry(subMap, child, value);
}catch(ClassCastException e){
nestRemapEntry(map, parent, child, value);
}
}
/**
* Remap an existing value at the parent key into the root value of a new scope inserted into the main
* map at the parent key. The value will be inserted into the new scope at the child key.
*
* @param map containing the parent entry which needs to be remapped as the root of a new scope
* @param parent the key at this level of the nested map
* @param child the key for the new value in the new scope
* @param value the object being inserted into the new scope
*/
protected static void nestRemapEntry(Map<String, Object> map, String parent, String child, Object value){
//retrieve the current value so we can remap it
Object current = map.remove(parent);
//create the new scope and map it into the parent key
Map<String, Object> subMap = new HashMap<String,Object>();
map.put(parent, subMap);
//readd the original entry at the root of the new scope and the new value into the child key of the scope
redactAndAddMapEntry(subMap, MAP_LEVEL_VALUE_KEY, current);
addNestedMapEntry(subMap, child, value);
}
/**
* Redact the value, if possible, and add it into the map using the specified key.
*
* @param map the nested map which will contain the object
* @param key the key in the nested map for the value
* @param value the object being inserted into the scope
*/
protected static void redactAndAddMapEntry(Map<String, Object> map, String key, Object value){
if(value instanceof String){
//at this point in time, only String can have private information redacted
map.put(key, Privacy.redactPrivateInformation(value.toString()));
}else{
map.put(key, value);
}
}
/**
* The method unregisters Keen's default JUL log handler to allow the application to have more control over
* how the KeenClient logs.
*
* @since 4.12.6
*/
protected static void disableKeenDefaultLogHandler(){
try{
Class<KeenLogging> clazz = KeenLogging.class;
Field loggerField = clazz.getDeclaredField("LOGGER");
Field handlerField = clazz.getDeclaredField("HANDLER");
loggerField.setAccessible(true);
handlerField.setAccessible(true);
java.util.logging.Logger keenLogger = (java.util.logging.Logger)loggerField.get(null);
java.util.logging.Handler keenHandler = (java.util.logging.Handler)handlerField.get(null);
keenLogger.removeHandler(keenHandler);
logger.debug("Removed KeenClient default JUL log handler {} from KeenLogging logger {}", keenHandler, keenLogger);
}catch(Exception e){
logger.error("Unable to remove KeenClient default JUL log handler from KeenLogging logger", e);
}
}
}