package org.rakam.automation;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.cookie.Cookie;
import io.netty.handler.codec.http.cookie.DefaultCookie;
import org.rakam.Mapper;
import org.rakam.collection.Event;
import org.rakam.config.EncryptionConfig;
import org.rakam.plugin.EventMapper;
import org.rakam.plugin.SyncEventMapper;
import org.rakam.plugin.user.User;
import org.rakam.plugin.user.UserStorage;
import org.rakam.util.CryptUtil;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.function.Supplier;
@Singleton
@Mapper(name = "Automation Event Processor", description = "Processes automation rules and take action if the user is completed the steps")
public class AutomationEventProcessor implements SyncEventMapper
{
private static final String PROPERTY_KEY = "_auto";
private static final String PROPERTY_ACTION_KEY = "_auto_action";
private final Provider<UserStorage> userStorageProvider;
private final Provider<UserAutomationService> serviceProvider;
private UserAutomationService service;
private UserStorage userStorage;
private final EncryptionConfig encryptionConfig;
private static final List<Cookie> clearData;
static {
DefaultCookie defaultCookie = new DefaultCookie(PROPERTY_KEY, "");
defaultCookie.setMaxAge(0);
clearData = ImmutableList.of(defaultCookie);
}
@Inject
public AutomationEventProcessor(
Provider<UserAutomationService> service,
Provider<UserStorage> storage,
EncryptionConfig encryptionConfig) {
this.encryptionConfig = encryptionConfig;
this.userStorageProvider = storage;
this.serviceProvider = service;
}
@Override
public void init()
{
this.userStorage = userStorageProvider.get();
this.service = serviceProvider.get();
}
@Override
public List<Cookie> map(Event event, RequestParams extraProperties, InetAddress sourceAddress, HttpHeaders responseHeaders) {
final List<AutomationRule> automationRules = service.list(event.project());
if (automationRules == null) {
return null;
}
ScenarioState[] value;
try {
value = extractState(extraProperties);
} catch (IllegalStateException e) {
return clearData;
}
boolean stateChanged = false;
List<String> actions = null;
ScenarioState[] newStates = null;
int newIdx = 0;
for (AutomationRule automationRule : automationRules) {
if (!automationRule.isActive) {
continue;
}
int ruleId = automationRule.id;
ScenarioState state = null;
if (value != null) {
for (ScenarioState scenarioState : value) {
if (ruleId == scenarioState.ruleId) {
state = scenarioState;
}
}
}
if (state == null) {
if (newStates == null) {
newStates = new ScenarioState[automationRules.size()];
if (value != null) {
for (ScenarioState scenarioState : value) {
newStates[newIdx++] = scenarioState;
}
}
}
state = new ScenarioState(ruleId, 0, 0);
newStates[newIdx++] = state;
}
AutomationRule.ScenarioStep scenarioStep = automationRule.scenarios.get(state.state);
if (event.collection().equals(scenarioStep.collection) && scenarioStep.filterPredicate.test(event)) {
stateChanged |= updateState(scenarioStep, state, event);
if (state.state >= automationRule.scenarios.size()) {
state.state = 0;
state.threshold = 0;
// state is already changed
if (actions == null) {
actions = new ArrayList<>();
}
for (AutomationRule.SerializableAction action : automationRule.actions) {
Supplier<User> supplier = new Supplier<User>() {
private User user;
@Override
public User get() {
if (user == null) {
String userAttr = event.getAttribute("_user");
if (userAttr != null) {
user = userStorage.getUser(event.project(), userAttr).join();
}
}
return user;
}
};
action.getAction().process(event.project(), supplier, action.value);
}
}
}
}
if (actions != null) {
StringBuilder builder = new StringBuilder();
Base64.Encoder encoder = Base64.getEncoder();
for (String action : actions) {
if (builder.length() != 0) {
builder.append(',');
}
try {
builder.append(encoder.encodeToString(action.getBytes("UTF-8")));
} catch (UnsupportedEncodingException e) {
throw Throwables.propagate(e);
}
}
responseHeaders.set(PROPERTY_ACTION_KEY, builder.toString());
}
return stateChanged ? ImmutableList.of(new DefaultCookie(PROPERTY_KEY, encodeState(newStates == null ? value : newStates))) : null;
}
private String encodeState(ScenarioState[] states) {
if (states == null) {
return null;
}
StringBuilder builder = new StringBuilder();
for (ScenarioState scenarioState : states) {
if (scenarioState != null) {
builder.append(scenarioState.ruleId).append(':').append(scenarioState.state).append(':').append(scenarioState.threshold);
}
}
String secureKey = CryptUtil.encryptWithHMacSHA1(builder.toString(), encryptionConfig.getSecretKey());
builder.append('|').append(secureKey);
return builder.toString();
}
private boolean updateState(AutomationRule.ScenarioStep scenarioStep, ScenarioState state, Event event) {
switch (scenarioStep.threshold.aggregation) {
case count:
String fieldName = scenarioStep.threshold.fieldName;
if (fieldName == null || (fieldName != null && event.getAttribute(fieldName) != null)) {
if (state.threshold == scenarioStep.threshold.value) {
state.state += 1;
state.threshold = 0;
} else {
state.threshold += 1;
}
return true;
}
break;
case sum:
Number val = event.getAttribute(scenarioStep.threshold.fieldName);
if (val != null) {
int newVal = state.threshold + val.intValue();
if (state.threshold <= newVal) {
state.state += 1;
state.threshold = 0;
} else {
state.threshold += newVal;
}
return true;
}
break;
}
return false;
}
private ScenarioState[] extractState(RequestParams extraProperties) throws IllegalStateException {
ScenarioState[] value;
String val = extraProperties.cookies().stream().filter(e -> e.name().equals(PROPERTY_KEY))
.findAny().map(e -> e.value()).orElse(null);
if (val == null) {
return null;
}
String[] cookie = val.split("\\|", 2);
if (cookie.length != 2 || !CryptUtil.encryptWithHMacSHA1(cookie[0], encryptionConfig.getSecretKey()).equals(cookie[1])) {
throw new IllegalStateException();
}
String[] values = cookie[0].split(",");
value = new ScenarioState[values.length];
for (int i = 0; i < values.length; i++) {
final String[] split = values[i].split(":", 3);
final ScenarioState scenarioState;
try {
scenarioState = new ScenarioState(Integer.parseInt(split[0]), Integer.parseInt(split[1]), Integer.parseInt(split[2]));
} catch (NumberFormatException e) {
throw new IllegalStateException();
}
value[i] = scenarioState;
}
return null;
}
private static class ScenarioState {
public final int ruleId;
public int state;
public int threshold;
public ScenarioState(int ruleId, int state, int threshold) {
this.ruleId = ruleId;
this.state = state;
this.threshold = threshold;
}
}
}