com.baasbox.service.scripting.ScriptingService.java Source code

Java tutorial

Introduction

Here is the source code for com.baasbox.service.scripting.ScriptingService.java

Source

/*
 * Copyright (c) 2014.
 *
 * BaasBox - info@baasbox.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.baasbox.service.scripting;

import com.baasbox.dao.ScriptsDao;
import com.baasbox.dao.exception.ScriptException;
import com.baasbox.dao.exception.SqlInjectionException;
import com.baasbox.service.webservices.HttpClientService;
import com.baasbox.service.scripting.base.*;
import com.baasbox.service.scripting.js.Json;
import com.baasbox.util.QueryParams;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.NullNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.baasbox.service.logging.BaasBoxLogger;

import play.libs.EventSource;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

import org.apache.commons.lang3.exception.ExceptionUtils;

import play.libs.WS;

/**
 * Created by Andrea Tortorella on 10/06/14.
 */
public class ScriptingService {
    private static Map<String, List<EventSource>> connectedLogListeners = new HashMap<String, List<EventSource>>();

    private static final ThreadLocal<String> MAIN = new ThreadLocal<>();

    /*
     * Script lifecycle
     *
     *  |  Install  |
     *       |
     *       v
     *  | Activate |<---.
     *       |          |
     *       v          |
     *    /LIVE/        |
     *       \          |
     *       v          |
     *  | Deactivate |--'
     *       |
     *       v
     *  | Uninstall |
     */

    public static ODocument resetStore(String name, JsonNode data) throws ScriptException {
        ScriptsDao dao = ScriptsDao.getInstance();
        ODocument script = dao.getByName(name);
        if (script == null)
            throw new ScriptException("Script not found");
        ODocument embedded;
        if (data == null || data.isNull()) {
            embedded = null;
            script.removeField(ScriptsDao.LOCAL_STORAGE);
        } else {
            embedded = new ODocument().fromJSON(data.toString());
            script.field(ScriptsDao.LOCAL_STORAGE, embedded);
        }
        dao.save(script);
        return embedded;
    }

    public static ODocument getStore(String name) throws ScriptException {
        ScriptsDao dao = ScriptsDao.getInstance();
        ODocument script = dao.getByName(name);
        if (script == null)
            throw new ScriptException("Script not found");
        return script.<ODocument>field(ScriptsDao.LOCAL_STORAGE);
    }

    private static ODocument updateStorageLocked(String name, boolean before, JsonCallback updaterFn)
            throws ScriptException {
        final ScriptsDao dao = ScriptsDao.getInstance();
        ODocument script = null;
        try {
            script = dao.getByNameLocked(name);
            if (script == null)
                throw new ScriptException("Script not found");
            ODocument retScript = before ? script.copy() : script;

            ODocument storage = script.<ODocument>field(ScriptsDao.LOCAL_STORAGE);

            Optional<ODocument> storage1 = Optional.ofNullable(storage);

            JsonNode current = storage1.map(ODocument::toJSON).map(Json.mapper()::readTreeOrMissing)
                    .orElse(NullNode.getInstance());
            if (current.isMissingNode())
                throw new ScriptEvalException("Error reading local storage as json");

            JsonNode updated = updaterFn.call(current);
            ODocument result;
            if (updated == null || updated.isNull()) {
                script.removeField(ScriptsDao.LOCAL_STORAGE);
            } else {
                result = new ODocument().fromJSON(updated.toString());
                script.field(ScriptsDao.LOCAL_STORAGE, result);
            }
            dao.save(script);
            ODocument field = retScript.field(ScriptsDao.LOCAL_STORAGE);
            return field;
        } finally {
            if (script != null) {
                script.unlock();
            }
        }
    }

    public static ODocument swap(String name, JsonCallback callback) throws ScriptException {
        return updateStorageLocked(name, false, callback);
    }

    public static ODocument trade(String name, JsonCallback updater) throws ScriptException {
        return updateStorageLocked(name, true, updater);
    }

    public static List<ODocument> list(QueryParams paramsFromQueryString) throws SqlInjectionException {
        ScriptsDao dao = ScriptsDao.getInstance();
        List<ODocument> scripts = dao.getAll(paramsFromQueryString);
        return scripts;
    }

    public static ScriptStatus update(String name, JsonNode code) throws ScriptException {
        if (code == null)
            throw new ScriptException("missing code");
        JsonNode codeNode = code.get(ScriptsDao.CODE);
        if (codeNode == null || !codeNode.isTextual()) {
            throw new ScriptException("missing code");
        }
        String source = codeNode.asText();
        return update(name, source);
    }

    public static ScriptStatus update(String name, String code) throws ScriptException {
        ScriptsDao dao = ScriptsDao.getInstance();
        updateCacheVersion();
        ODocument updated = dao.update(name, code);
        compile(updated, false);

        ScriptStatus status;
        ScriptCall install = ScriptCall.install(updated);
        try {
            ScriptResult result = invoke(install);
            status = result.toScriptStatus();
            if (!status.ok) {
                updateCacheVersion();
                dao.revertToLastVersion(updated);

            }
        } catch (ScriptEvalException e) {
            if (BaasBoxLogger.isDebugEnabled())
                BaasBoxLogger.debug("Script installation failed: deleting");
            updateCacheVersion();
            dao.invalidate(updated);
            dao.revertToLastVersion(updated);
            throw e;
        }
        return status;
    }

    /**
     * Creates a new script object
     * @param script
     * @return
     * @throws ScriptException
     */
    public static ScriptStatus create(JsonNode script) throws ScriptException {
        if (BaasBoxLogger.isTraceEnabled())
            BaasBoxLogger.trace("Method start");

        if (BaasBoxLogger.isDebugEnabled())
            BaasBoxLogger.debug("Creating script");
        ScriptsDao dao = ScriptsDao.getInstance();

        ODocument doc = createScript(dao, script);
        compile(doc, true);
        if (BaasBoxLogger.isDebugEnabled())
            BaasBoxLogger.debug("Script created");

        if (BaasBoxLogger.isDebugEnabled())
            BaasBoxLogger.debug("Script installing");
        ScriptStatus status;
        ScriptCall installation = ScriptCall.install(doc);
        try {

            ScriptResult res = invoke(installation);
            status = res.toScriptStatus();
            if (!status.ok) {
                if (BaasBoxLogger.isDebugEnabled())
                    BaasBoxLogger.debug("Script installation aborted by the script");
                doc.delete();
            }
        } catch (ScriptEvalException e) {
            if (BaasBoxLogger.isDebugEnabled())
                BaasBoxLogger.debug("Script installation failed: deleting - " + ExceptionUtils.getStackTrace(e));
            doc.delete();
            throw new ScriptException(e);
        }
        if (BaasBoxLogger.isTraceEnabled())
            BaasBoxLogger.trace("Method end");
        return status;
    }

    /**
     * Returns a script object corresponding to name
     * @param name
     * @return
     */
    public static ODocument get(String name) {
        ScriptsDao dao = ScriptsDao.getInstance();
        ODocument script = dao.getByName(name);
        return script;
    }

    public static ODocument get(String name, boolean onlyvalid, boolean active) throws ScriptException {
        ScriptsDao dao = ScriptsDao.getInstance();
        ODocument script = dao.getByName(name);
        if (script != null) {
            if (onlyvalid && script.<Boolean>field(ScriptsDao.INVALID)) {
                throw new ScriptException("Script is in invalid state");
            }
            if (active && !(script.<Boolean>field(ScriptsDao.ACTIVE))) {
                throw new ScriptEvalException("Script is not active");
            }
        }
        return script;
    }

    /**
     * Deletes a script object with name
     * @param name
     * @return
     */
    public static boolean delete(String name) throws ScriptException {
        updateCacheVersion();
        ScriptsDao dao = ScriptsDao.getInstance();
        ODocument script = dao.getByName(name);
        //script not found
        if (script == null) {
            return false;
        }

        ScriptCall uninstall = ScriptCall.uninstall(script);
        try {
            invoke(uninstall);
            return dao.delete(name);
        } catch (ScriptException e) {
            dao.invalidate(script);
            throw e;
        }
    }

    public static boolean forceDelete(String name) throws ScriptException {
        updateCacheVersion();
        ScriptsDao dao = ScriptsDao.getInstance();
        return dao.delete(name);
    }

    public static Boolean activate(String name, boolean activate) {
        updateCacheVersion();
        ScriptsDao dao = ScriptsDao.getInstance();
        ODocument doc = dao.getByName(name);
        if (doc == null) {
            return null;
        }
        return dao.activate(doc, activate);
    }

    public static ScriptResult invoke(ScriptCall call) throws ScriptEvalException {
        if (BaasBoxLogger.isDebugEnabled())
            BaasBoxLogger.debug("Invoking script: " + call.scriptName);
        MAIN.set(call.scriptName);
        BaasboxScriptEngine engine = call.engine();
        try {
            ScriptResult res = engine.eval(call);
            return res;
        } catch (Exception e) {
            if (e instanceof ScriptEvalException) {
                throw (ScriptEvalException) e;
            } else {
                throw new ScriptEvalException(ExceptionUtils.getMessage(e), e);
            }
        } finally {
            MAIN.set(null);
        }
    }

    //    public static void publishLog(String to,JsonNode message){
    //        List<EventSource> listeners = connectedLogListeners.get(to);
    //        if (Logger.isTraceEnabled())Logger.trace("Publishing message");
    //        if (listeners==null||listeners.isEmpty()) return;
    //
    //        for (EventSource s:listeners){
    //            if (s!=null){
    //
    //                s.sendData(message.toString());
    //            }
    //        }
    //    }
    //
    //    public static void disconnectLogListener(String name, EventSource current) {
    //        List<EventSource> logs = connectedLogListeners.get(name);
    //        if (logs == null){
    //            return;
    //        } else if (logs.contains(current)){
    //            logs.remove(current);
    //            if (Logger.isTraceEnabled()) Logger.trace("Disconnected: "+name);
    //            connectedLogListeners.put(name,logs);
    //        }
    //    }

    /**
     * Compiles a script without emitting any event
     * @param doc
     * @throws ScriptEvalException
     */
    private static void compile(ODocument doc, boolean dropOnFailure) throws ScriptEvalException {
        if (BaasBoxLogger.isDebugEnabled())
            BaasBoxLogger.debug("Start Compile");
        ScriptCall compile = ScriptCall.compile(doc);
        try {
            invoke(compile);
            if (BaasBoxLogger.isDebugEnabled())
                BaasBoxLogger.debug("End Compile");
        } catch (ScriptEvalException e) {
            BaasBoxLogger.error("Failed Script compilation");
            if (dropOnFailure) {
                doc.delete();
            } else {
                ScriptsDao dao = ScriptsDao.getInstance();

                dao.revertToLastVersion(doc);
            }
            if (BaasBoxLogger.isDebugEnabled())
                BaasBoxLogger.debug("Script delete");
            throw e;
        }
    }

    private static ODocument createScript(ScriptsDao dao, JsonNode node) throws ScriptException {
        updateCacheVersion();
        String lang = node.get(ScriptsDao.LANG).asText();
        ScriptLanguage language = ScriptLanguage.forName(lang);
        String name = node.get(ScriptsDao.NAME).asText();
        String code = node.get(ScriptsDao.CODE).asText();
        JsonNode initialStorage = node.get(ScriptsDao.LOCAL_STORAGE);
        JsonNode library = node.get(ScriptsDao.LIB);
        boolean isLibrary = library == null ? false : library.asBoolean();
        JsonNode activeNode = node.get(ScriptsDao.ACTIVE);
        boolean active = activeNode == null ? false : activeNode.asBoolean();
        JsonNode encoded = node.get(ScriptsDao.ENCODED);
        ODocument doc;
        if (encoded != null && encoded.isTextual()) {
            String encodedValue = encoded.asText();
            doc = dao.create(name, language.name, code, isLibrary, active, initialStorage, encodedValue);
        } else {
            doc = dao.create(name, language.name, code, isLibrary, active, initialStorage);
        }
        return doc;
    }

    public static JsonNode callJsonSync(JsonNode req) throws Exception {
        JsonNode url = req.get("url");
        JsonNode method = req.get("method");
        JsonNode timeout = req.get("timeout");

        if (url == null || url instanceof NullNode)
            throw new IllegalArgumentException("Missing URL to call");
        if (method == null || method instanceof NullNode)
            throw new IllegalArgumentException("Missing method to use when calling the URL");

        return callJsonSync(url.asText(), method.asText(), mapJson(req.get("params")), mapJson(req.get("headers")),
                req.get("body"), (timeout != null && timeout.isNumber()) ? timeout.asInt() : null);
    }

    private static Map<String, List<String>> mapJson(JsonNode node) {
        if (node == null || node instanceof NullNode) {
            return null;
        }
        if (node.isObject()) {
            Map<String, List<String>> ret = new LinkedHashMap<>();
            node.fieldNames().forEachRemaining((field) -> {
                JsonNode jsonNode = node.get(field);
                List<String> cur = ret.get(field);
                if (cur == null) {
                    cur = new LinkedList<>();
                    ret.put(field, cur);
                }
                append(cur, jsonNode);
            });
            return ret;
        }
        return null;
    }

    private static void append(List<String> list, JsonNode node) {
        if (node == null || node.isNull() || node.isMissingNode() || node.isObject())
            return;
        if (node.isValueNode())
            list.add(node.asText());
        if (node.isArray()) {
            node.forEach((n) -> {
                if (n != null && (!n.isNull()) && (!n.isMissingNode()) && n.isValueNode())
                    list.add(n.toString());
            });
        }
    }

    private static JsonNode callJsonSync(String url, String method, Map<String, List<String>> params,
            Map<String, List<String>> headers, JsonNode body, Integer timeout) throws Exception {
        try {
            ObjectNode node = Json.mapper().createObjectNode();
            WS.Response resp = null;

            long startTime = System.nanoTime();
            if (timeout == null) {
                resp = HttpClientService.callSync(url, method, params, headers,
                        body == null ? null : (body.isValueNode() ? body.toString() : body));
            } else {
                resp = HttpClientService.callSync(url, method, params, headers,
                        body == null ? null : (body.isValueNode() ? body.toString() : body), timeout);
            }
            long endTime = System.nanoTime();

            int status = resp.getStatus();
            node.put("status", status);
            node.put("execution_time", (endTime - startTime) / 1000000L);

            String header = resp.getHeader("Content-Type");
            if (header == null || header.startsWith("text")) {
                node.put("body", resp.getBody());
            } else if (header.startsWith("application/json")) {
                node.put("body", resp.asJson());
            } else {
                node.put("body", resp.getBody());
            }

            return node;
        } catch (Exception e) {
            BaasBoxLogger.error("failed to connect: " + ExceptionUtils.getMessage(e));
            throw e;
        }

    }

    /// cache management

    private static final AtomicLong SCRIPT_UPDATE_COUNTER = new AtomicLong(Long.MIN_VALUE);

    private static void updateCacheVersion() {
        SCRIPT_UPDATE_COUNTER.incrementAndGet();
    }

    public static long getCacheVersion() {
        return SCRIPT_UPDATE_COUNTER.get();
    }

    public static String main() {
        return MAIN.get();
    }

}