package io.lumify.core.formula;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.inject.Inject;
import io.lumify.core.config.Configuration;
import io.lumify.core.exception.LumifyException;
import io.lumify.core.model.ontology.OntologyRepository;
import io.lumify.core.util.ClientApiConverter;
import io.lumify.core.util.LumifyLogger;
import io.lumify.core.util.LumifyLoggerFactory;
import io.lumify.web.clientapi.model.ClientApiOntology;
import io.lumify.web.clientapi.model.ClientApiVertex;
import io.lumify.web.clientapi.model.util.ObjectMapperFactory;
import org.apache.commons.io.IOUtils;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.securegraph.Authorizations;
import org.securegraph.Vertex;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import static com.google.common.base.Preconditions.checkNotNull;
public class FormulaEvaluator {
private static final LumifyLogger LOGGER = LumifyLoggerFactory.getLogger(FormulaEvaluator.class);
private Configuration configuration;
private OntologyRepository ontologyRepository;
private static final ThreadLocal<Map<String, ScriptableObject>> threadLocalScope = new ThreadLocal<>();
@Inject
public FormulaEvaluator(Configuration configuration, OntologyRepository ontologyRepository) {
this.configuration = configuration;
this.ontologyRepository = ontologyRepository;
}
@Override
protected void finalize() throws Throwable {
super.finalize();
if (Context.getCurrentContext() != null) {
LOGGER.warn("close() method not called to clean up JavaScript Context");
}
}
public void close() {
synchronized (threadLocalScope) {
if (Context.getCurrentContext() != null) {
Context.exit();
threadLocalScope.remove();
}
}
}
public String evaluateTitleFormula(Vertex vertex, UserContext userContext, Authorizations authorizations) {
return evaluateFormula("Title", vertex, userContext, authorizations);
}
public String evaluateTimeFormula(Vertex vertex, UserContext userContext, Authorizations authorizations) {
return evaluateFormula("Time", vertex, userContext, authorizations);
}
public String evaluateSubtitleFormula(Vertex vertex, UserContext userContext, Authorizations authorizations) {
return evaluateFormula("Subtitle", vertex, userContext, authorizations);
}
private String evaluateFormula(String type, Vertex vertex, UserContext userContext, Authorizations authorizations) {
checkNotNull(userContext, "userContext cannot be null");
Scriptable scope = getScriptable(userContext.getLocale(), userContext.getTimeZone());
String json = toJson(vertex, userContext.getWorkspaceId(), authorizations);
Function function = (Function) scope.get("evaluate" + type + "FormulaJson", scope);
Object result = function.call(Context.getCurrentContext(), scope, scope, new Object[]{json});
return (String) Context.jsToJava(result, String.class);
}
protected Scriptable getScriptable(Locale locale, String timeZone) {
synchronized (threadLocalScope) {
Map<String, ScriptableObject> map = threadLocalScope.get();
if (map == null) {
map = new HashMap<>();
threadLocalScope.set(map);
}
String mapKey = locale.toString() + timeZone;
ScriptableObject scope = map.get(mapKey);
if (scope == null) {
scope = setupContext(getOntologyJson(), getConfigurationJson(locale), timeZone);
map.put(mapKey, scope);
}
return scope;
}
}
protected static ScriptableObject setupContext(String ontologyJson, String configurationJson, String timeZone) {
if (Context.getCurrentContext() != null) {
Context.exit();
threadLocalScope.remove();
}
Context context = Context.enter();
context.setLanguageVersion(Context.VERSION_1_6);
final RequireJsSupport browserSupport = new RequireJsSupport();
ScriptableObject scope = context.initStandardObjects(browserSupport, true);
try {
scope.put("ONTOLOGY_JSON", scope, Context.toObject(ontologyJson, scope));
scope.put("CONFIG_JSON", scope, Context.toObject(configurationJson, scope));
scope.put("USERS_TIMEZONE", scope, Context.toObject(timeZone, scope));
} catch (Exception e) {
throw new LumifyException("Json resource not available", e);
}
String[] names = new String[]{"print", "load", "consoleWarn", "consoleError", "readFile"};
browserSupport.defineFunctionProperties(names, scope.getClass(), ScriptableObject.DONTENUM);
Scriptable argsObj = context.newArray(scope, new Object[]{});
scope.defineProperty("arguments", argsObj, ScriptableObject.DONTENUM);
loadJavaScript(scope);
scope.sealObject();
return scope;
}
private static void loadJavaScript(ScriptableObject scope) {
evaluateFile(scope, "libs/underscore.js");
evaluateFile(scope, "libs/r.js");
evaluateFile(scope, "libs/windowTimers.js");
evaluateFile(scope, "loader.js");
}
protected String getOntologyJson() {
ClientApiOntology result = ontologyRepository.getClientApiObject();
try {
return ObjectMapperFactory.getInstance().writeValueAsString(result);
} catch (JsonProcessingException ex) {
throw new LumifyException("Could not evaluate JSON: " + result, ex);
}
}
protected String getConfigurationJson(Locale locale) {
return configuration.toJSON(locale).toString();
}
private static Object evaluateFile(ScriptableObject scope, String filename) {
InputStream is = FormulaEvaluator.class.getResourceAsStream(filename);
if (is != null) {
try {
return Context.getCurrentContext().evaluateString(scope, IOUtils.toString(is), filename, 0, null);
} catch (IOException e) {
LOGGER.error("File not readable %s", filename);
}
} else LOGGER.error("File not found %s", filename);
return null;
}
protected String toJson(Vertex vertex, String workspaceId, Authorizations authorizations) {
ClientApiVertex v = ClientApiConverter.toClientApiVertex(vertex, workspaceId, authorizations);
return v.toString();
}
public static class UserContext {
private final Locale locale;
private final String timeZone;
private final String workspaceId;
public UserContext(Locale locale, String timeZone, String workspaceId) {
this.locale = locale == null ? Locale.getDefault() : locale;
this.timeZone = timeZone;
this.workspaceId = workspaceId;
}
public Locale getLocale() {
return locale;
}
public String getTimeZone() {
return timeZone;
}
public String getWorkspaceId() {
return workspaceId;
}
}
}