/*******************************************************************************
* Copyright (c) 2014 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v1.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package org.jboss.tools.usage.internal.event;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Calendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.UUID;
import org.eclipse.core.runtime.IPath;
import org.jboss.tools.usage.event.UsageEvent;
import org.jboss.tools.usage.event.UsageEventType;
import org.jboss.tools.usage.event.UsageReporter;
import org.jboss.tools.usage.internal.JBossToolsUsageActivator;
import org.jboss.tools.usage.internal.preferences.GlobalUsageSettings;
/**
* Represents a register of tracking event types.
* Each event type must be registered via this register before the corresponding event can be sent.
*
* @author Alexey Kazakov
*/
public class EventRegister {
// Event type component name
private static final String EVENT_TYPE_COMPONENT_NAME = "cmp";
// Event type component version
private static final String EVENT_TYPE_VERSION = "v";
// Event type category name
private static final String EVENT_TYPE_CATEGORY_NAME = "ct";
// Event type action name
private static final String EVENT_TYPE_ACTION_NAME = "a";
// Event type label description
private static final String EVENT_TYPE_LABEL_DESCRIPTION = "l";
// Event type value description
private static final String EVENT_TYPE_VALUE_DESCRIPTION = "vl";
// Event type date when last time sent
private static final String EVENT_TYPE_DATE = "d";
// How many times sent during the day
private static final String EVENT_TYPE_COUNT = "c";
// The sum of all values collected during the day
private static final String EVENT_TYPE_VALUE_SUM = "s";
// Label value used by countEvent()
private static final String EVENT_TYPE_COUNT_EVENT_LABEL = "cl";
private static EventRegister INSTANCE = new EventRegister();
protected Map<EventTypeKey, UsageEventProperties> eventPropertyStorage;
protected Map<UsageEventType, Set<UsageEventProperties>> eventPropertyStorageByType;
protected Set<UsageEventType> eventTypes;
protected EventRegister() {
}
public static EventRegister getInstance() {
return INSTANCE;
}
/**
* Registers the event type
*
* @param type
*/
public synchronized void registerEvent(UsageEventType type) {
if(eventTypes==null) {
eventTypes = new HashSet<UsageEventType>();
}
eventTypes.add(type);
}
/**
* Returns Result.okToSend == true if the event can be sent
*
* @param event
* @param settings
* @param countEvent
* @return
*/
public synchronized Result checkTrackData(UsageEvent event, GlobalUsageSettings settings, boolean countEvent) {
Result result = new Result();
if(settings.isReportingEnabled()) {
init();
// Check if the event type has been registered
if(eventTypes!=null && eventTypes.contains(event.getType())) {
long today = getCurrentTime();
EventTypeKey eventTypeKey = new EventTypeKey(event.getType(), event.getLabel());
UsageEventProperties preferenceProperties = eventPropertyStorage.get(eventTypeKey);
if(preferenceProperties==null) {
// First time use of the event
preferenceProperties = new UsageEventProperties();
preferenceProperties.type = event.getType();
preferenceProperties.date = today;
if(countEvent) {
preferenceProperties.value = getValue(event);
} else {
preferenceProperties.count = 1;
}
String label = event.getLabel();
if(label == null) {
label = eventTypeKey.label;
}
preferenceProperties.countEventLabel = label;
eventPropertyStorage.put(eventTypeKey, preferenceProperties);
addTypePropetiesToStorage(preferenceProperties, event.getType());
} else {
if(!isSameDay(today, preferenceProperties.date)) {
if(countEvent) {
result.previousSumOfValues = preferenceProperties.value;
preferenceProperties.value = getValue(event);
} else {
preferenceProperties.count = 1;
}
} else {
if(countEvent) {
preferenceProperties.value = preferenceProperties.value + getValue(event);
} else {
preferenceProperties.count++;
}
}
preferenceProperties.date = today;
}
// Update preferenceProperties in workspace
saveProperties(preferenceProperties);
// Check remote usage.properties
result.countEventLabel = preferenceProperties.countEventLabel;
result.okToSend = checkRemoteSettings(settings, event.getType(), event.getLabel(), preferenceProperties.count) && (!countEvent || result.previousSumOfValues>0);
} else {
JBossToolsUsageActivator.getDefault().getLogger().error("Event type [" + event.toString() + "] is not registered and will be ignored.", false);
}
}
return result;
}
public synchronized UsageEventType[] getRegisteredEventTypes() {
if(eventTypes==null) {
return new UsageEventType[0];
}
return eventTypes.toArray(new UsageEventType[eventTypes.size()]);
}
public synchronized Set<Result> checkCountEvent(UsageEventType type, GlobalUsageSettings settings) {
Set<Result> results = new HashSet<Result>();
if(settings.isReportingEnabled()) {
init();
if(eventTypes!=null && eventTypes.contains(type)) {
long today = getCurrentTime();
Set<UsageEventProperties> preferencePropertiesSet = eventPropertyStorageByType.get(type);
if(preferencePropertiesSet!=null) {
for (UsageEventProperties preferenceProperties : preferencePropertiesSet) {
if(!isSameDay(today, preferenceProperties.date)) {
Result result = new Result();
result.previousSumOfValues = preferenceProperties.value;
result.countEventLabel = preferenceProperties.countEventLabel;
result.okToSend = result.previousSumOfValues>0 && checkRemoteSettings(settings, type, preferenceProperties.countEventLabel, preferenceProperties.count);
preferenceProperties.count = 0;
preferenceProperties.value = 0;
preferenceProperties.date = today;
results.add(result);
saveProperties(preferenceProperties);
}
}
}
}
}
return results;
}
private int getValue(UsageEvent event) {
return event.getValue()!=null?event.getValue():1;
}
/**
* Check remote settings.
*
* Format of the settings:
*
* For category (required) + action (optional):
* <category>[.<action>]=-n/0/+n
* -n = no limit (used by default)
* 0 = never send
* +n = maximum a day
*
* Additionally, an optional label can be used as an extra parameter to enable or disable some particular labels
* <category>.<action>[.<label>]=-n/0
* -n = no limit (used by default)
* 0 = never send
*
* '.' can be used in category/label/action names.
* But all whitespace characters should be replaced by '-' in the usage.properties file though users still can send names with whitespace.
* So property "server.new.AS-7.1" is equals to Event with Category: "server", Action: "new", Label: "AS 7.1"
*
*
* Example:
* server=0
* server.new=3
* server.new.AS7-1=-1
* sever.run=5
* It means:
* jst.palette - enabled, no limit (there is no property "jst" or "jst.palette" so the default value "no limit" is used)
* server.new.AS7-1 - enabled, no limit ("server.new.AS7-1=-1" is used)
* server.new.AS6-2 - no more than 3 times a day (there is no "server.new.AS6-2" property so the existing "server.new=3" is used)
* server.run.AS-1 - no more than 5 times a day (there is no "server.run.AS-1" property so the existing "sever.run=5" is used)
* server.remove - disabled (there is no "server.remove" property so the existing "server=0" is used)
*
*
* @param settings
* @param type
* @param label
* @param count
* @return
*/
private boolean checkRemoteSettings(GlobalUsageSettings settings, UsageEventType type, String label, int count) {
try {
Map<Object, Object> map = settings.getRemoteSettings();
String actionKey = type.getCategoryName() + "." + type.getActionName();
String value = null;
if(label!=null) {
String labelKey = actionKey + "." + label;
value = (String)map.get(labelKey);
}
if(value==null) {
value = (String)map.get(actionKey);
if(value==null) {
value = (String)map.get(type.getCategoryName());
}
}
if(value!=null && !value.isEmpty()) {
try {
int v = Integer.valueOf(value);
return v<0 || count<=v;
} catch (NumberFormatException e) {
JBossToolsUsageActivator.getDefault().getLogger().error(e);
}
}
return true;
} catch (IOException e) {
JBossToolsUsageActivator.getDefault().getLogger().error(e, true);
}
return false;
}
protected long getCurrentTime() {
return System.currentTimeMillis();
}
private boolean isSameDay(long date1, long date2) {
Calendar calendar1 = Calendar.getInstance();
Calendar calendar2 = Calendar.getInstance();
calendar1.setTimeInMillis(date1);
calendar2.setTimeInMillis(date2);
return calendar1.get(Calendar.YEAR) == calendar2.get(Calendar.YEAR) && calendar1.get(Calendar.DAY_OF_YEAR) == calendar2.get(Calendar.DAY_OF_YEAR);
}
protected File getStorageDirectory() {
JBossToolsUsageActivator plugin = JBossToolsUsageActivator.getDefault();
if(plugin != null) {
//The plug-in instance can be null at shutdown, when the plug-in is stopped.
IPath path = plugin.getStateLocation();
File file = new File(path.toFile(), "events"); //$NON-NLS-1$
if(!file.exists()) {
file.mkdirs();
}
return file;
} else {
return null;
}
}
private File getStorageFile(UsageEventProperties properties) {
File directory = getStorageDirectory();
if(directory!=null) {
String name = properties.type.getCategoryName() + "-" + properties.type.getActionName() + "-" + properties.countEventLabel;
try {
name = URLEncoder.encode(name, "UTF-8");
if(name.length()>40) {
name = (UUID.nameUUIDFromBytes(name.getBytes())).toString();
}
return new File(directory, name);
} catch (UnsupportedEncodingException e) {
JBossToolsUsageActivator.getDefault().getLogger().error(e);
}
}
return null;
}
private boolean saveProperties(UsageEventProperties properties) {
File file = getStorageFile(properties);
if(file!=null) {
if(file.isFile()) {
file.delete();
}
FileWriter writer = null;
try {
UsageEventType type = properties.type;
writer = new FileWriter(file);
Properties pr = new Properties();
pr.put(EVENT_TYPE_COMPONENT_NAME, type.getComponentName());
pr.put(EVENT_TYPE_VERSION, type.getComponentVersion());
pr.put(EVENT_TYPE_CATEGORY_NAME, type.getCategoryName());
pr.put(EVENT_TYPE_ACTION_NAME, type.getActionName());
if(type.getLabelDescription()!=null) {
pr.put(EVENT_TYPE_LABEL_DESCRIPTION, type.getLabelDescription());
if(type.getValueDescription()!=null) {
pr.put(EVENT_TYPE_VALUE_DESCRIPTION, type.getValueDescription());
}
}
pr.put(EVENT_TYPE_DATE, "" + properties.date);
pr.put(EVENT_TYPE_COUNT, "" + properties.count);
pr.put(EVENT_TYPE_VALUE_SUM, "" + properties.value);
pr.put(EVENT_TYPE_COUNT_EVENT_LABEL, properties.countEventLabel);
pr.store(writer, null);
return true;
} catch (IOException e) {
JBossToolsUsageActivator.getDefault().getLogger().error(e);
} finally {
if(writer!=null) {
try {
writer.close();
} catch (IOException e) {
}
}
}
}
return false;
}
protected void init() {
if(eventPropertyStorage==null) {
eventPropertyStorage = new HashMap<EventTypeKey, UsageEventProperties>();
eventPropertyStorageByType = new HashMap<UsageEventType, Set<UsageEventProperties>>();
File directory = getStorageDirectory();
if(directory!=null) {
File[] files = directory.listFiles();
long sixMonthsAgo = System.currentTimeMillis() - 1000L*60*60*24*132;
for (File file : files) {
if(file.isFile()) {
FileReader reader = null;
try {
reader = new FileReader(file);
Properties pr = new Properties();
pr.load(reader);
long date = Long.parseLong(pr.getProperty(EVENT_TYPE_DATE, "0"));
if(date<sixMonthsAgo) {
file.delete(); // This event type has not been used at least for the last six months, so let's remove it.
} else {
String component = pr.getProperty(EVENT_TYPE_COMPONENT_NAME);
String version = pr.getProperty(EVENT_TYPE_VERSION);
String category = pr.getProperty(EVENT_TYPE_CATEGORY_NAME);
String action = pr.getProperty(EVENT_TYPE_ACTION_NAME);
String label = pr.getProperty(EVENT_TYPE_LABEL_DESCRIPTION);
String value = pr.getProperty(EVENT_TYPE_VALUE_DESCRIPTION);
int count = Integer.parseInt(pr.getProperty(EVENT_TYPE_COUNT, "0"));
int valueSum = Integer.parseInt(pr.getProperty(EVENT_TYPE_VALUE_SUM, "0"));
String countLabel = pr.getProperty(EVENT_TYPE_COUNT_EVENT_LABEL);
if(component!=null && version!=null && category!=null && action!=null && countLabel!=null) {
UsageEventType type = new UsageEventType(component, version, category, action, label, value);
UsageEventProperties properties = new UsageEventProperties();
properties.type = type;
properties.date = date;
properties.count = count;
properties.value = valueSum;
properties.countEventLabel = countLabel;
eventPropertyStorage.put(new EventTypeKey(type, countLabel), properties);
addTypePropetiesToStorage(properties, type);
}
}
} catch (IOException e) {
JBossToolsUsageActivator.getDefault().getLogger().error(e);
} finally {
if(reader!=null) {
try {
reader.close();
} catch (IOException e) {
}
}
}
}
}
}
}
}
private void addTypePropetiesToStorage(UsageEventProperties properties, UsageEventType type) {
Set<UsageEventProperties> typeProperties = eventPropertyStorageByType.get(type);
if(typeProperties==null) {
typeProperties = new HashSet<UsageEventProperties>();
}
typeProperties.add(properties);
eventPropertyStorageByType.put(type, typeProperties);
}
private static class EventTypeKey {
UsageEventType type;
String label;
/**
* Label may be null
* @param type
* @param label
*/
EventTypeKey(UsageEventType type, String label) {
this.type = type;
if(label==null) {
label = UsageReporter.NOT_APPLICABLE_LABEL;
}
this.label = label;
}
@Override
public int hashCode() {
return (type.getComponentName() + type.getCategoryName() + type.getActionName() + label).hashCode();
}
@Override
public boolean equals(Object obj) {
if(this == obj) {
return true;
}
if(obj instanceof EventTypeKey) {
EventTypeKey key = (EventTypeKey)obj;
return type.equals(key.type) && label.equals(key.label);
}
return false;
}
@Override
public String toString() {
return "Type: " + type.toString() + "; Label: " + label;
}
}
public static class Result {
private boolean okToSend;
private int previousSumOfValues;
private String countEventLabel;
/**
* Returns true if this event can be sent
*
* @return
*/
public boolean isOkToSend() {
return okToSend;
}
/**
* Returns the sum of all values of this event collected during the day when this event was send last time.
* It's used for daily event tracking. The daily event should be sent if the returned number is bigger than 0.
* Category, action names and labels are taken into account when values are being counted. If the label is null then "N/A" is used.
* @return
*/
public int getPreviousSumOfValues() {
return previousSumOfValues;
}
public String getCountEventLabel() {
return countEventLabel;
}
}
private static class UsageEventProperties {
UsageEventType type;
long date; // The date when the event with this type was sent last time
int count; // How many times events with this type and even label were sent on the day of the last use
int value; // The sum of all values collected during the day
String countEventLabel;
}
}