001/*
002 * (C) Copyright 2015-2016 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *  Stephane Lacoin <slacoin@nuxeo.com>
018 *  Vladimir Pasquier <vpasquier@nuxeo.com>
019 */
020package org.nuxeo.automation.scripting.internals;
021
022import java.io.IOException;
023import java.io.InputStream;
024import java.util.ArrayList;
025import java.util.Collections;
026import java.util.HashMap;
027import java.util.List;
028import java.util.Map;
029
030import javax.script.Invocable;
031import javax.script.ScriptEngine;
032import javax.script.ScriptEngineManager;
033import javax.script.ScriptException;
034import javax.script.SimpleScriptContext;
035
036import org.apache.commons.io.IOUtils;
037import org.nuxeo.automation.scripting.api.AutomationScriptingConstants;
038import org.nuxeo.automation.scripting.api.AutomationScriptingService;
039import org.nuxeo.ecm.automation.AutomationService;
040import org.nuxeo.ecm.automation.OperationException;
041import org.nuxeo.ecm.automation.OperationType;
042import org.nuxeo.ecm.automation.context.ContextHelper;
043import org.nuxeo.ecm.automation.context.ContextService;
044import org.nuxeo.ecm.automation.core.Constants;
045import org.nuxeo.ecm.automation.core.scripting.DateWrapper;
046import org.nuxeo.ecm.automation.core.scripting.PrincipalWrapper;
047import org.nuxeo.ecm.core.api.CoreSession;
048import org.nuxeo.ecm.core.api.NuxeoException;
049import org.nuxeo.ecm.core.api.NuxeoPrincipal;
050import org.nuxeo.runtime.api.Framework;
051
052/**
053 * @since 7.2
054 */
055public class AutomationScriptingServiceImpl implements AutomationScriptingService {
056
057    protected String jsWrapper = null;
058
059    protected ScriptOperationContext operationContext;
060
061    protected String getJSWrapper(boolean refresh) throws OperationException {
062        if (jsWrapper == null || refresh) {
063            StringBuffer sb = new StringBuffer();
064            AutomationService as = Framework.getService(AutomationService.class);
065            Map<String, List<String>> opMap = new HashMap<>();
066            List<String> flatOps = new ArrayList<>();
067            List<String> ids = new ArrayList<>();
068            for (OperationType op : as.getOperations()) {
069                ids.add(op.getId());
070                if (op.getAliases() != null) {
071                    Collections.addAll(ids, op.getAliases());
072                }
073            }
074            // Create js object related to operation categories
075            for (String id : ids) {
076                parseAutomationIDSForScripting(opMap, flatOps, id);
077            }
078            for (String obName : opMap.keySet()) {
079                List<String> ops = opMap.get(obName);
080                sb.append("\nvar ").append(obName).append("={};");
081                for (String opId : ops) {
082                    generateFunction(sb, opId);
083                }
084            }
085            for (String opId : flatOps) {
086                generateFlatFunction(sb, opId);
087            }
088            jsWrapper = sb.toString();
089        }
090        return jsWrapper;
091    }
092
093    @Override
094    public void setOperationContext(ScriptOperationContext ctx) {
095        this.operationContext = operationContexts.get();
096        this.operationContext = wrapContext(ctx);
097    }
098
099    protected ScriptOperationContext wrapContext(ScriptOperationContext ctx) {
100        ctx.replaceAll((key, value) -> WrapperHelper.wrap(value, ctx.getCoreSession()));
101        return ctx;
102    }
103
104    @Override
105    public String getJSWrapper() throws OperationException {
106        return getJSWrapper(false);
107    }
108
109    protected final ThreadLocal<ScriptEngine> engines = new ThreadLocal<ScriptEngine>() {
110        @Override
111        protected ScriptEngine initialValue() {
112            return Framework.getService(ScriptEngineManager.class).getEngineByName(
113                    AutomationScriptingConstants.NX_NASHORN);
114        }
115    };
116
117    protected final ThreadLocal<ScriptOperationContext> operationContexts = new ThreadLocal<ScriptOperationContext>() {
118        @Override
119        protected ScriptOperationContext initialValue() {
120            return new ScriptOperationContext();
121        }
122    };
123
124    @Override
125    public void run(InputStream in, CoreSession session) throws ScriptException, OperationException {
126        try {
127            run(IOUtils.toString(in, "UTF-8"), session);
128        } catch (IOException e) {
129            throw new NuxeoException(e);
130        }
131    }
132
133    @Override
134    public void run(String script, CoreSession session) throws ScriptException, OperationException {
135        ScriptEngine engine = engines.get();
136        engine.setContext(new SimpleScriptContext());
137        engine.eval(getJSWrapper());
138
139        // Initialize Operation Context
140        if (operationContext == null) {
141            operationContext = operationContexts.get();
142            operationContext.setCoreSession(session);
143        }
144
145        // Injecting Automation Mapper 'automation'
146        AutomationMapper automationMapper = new AutomationMapper(session, operationContext);
147        engine.put(AutomationScriptingConstants.AUTOMATION_MAPPER_KEY, automationMapper);
148
149        // Inject operation context vars in 'Context'
150        engine.put(AutomationScriptingConstants.AUTOMATION_CTX_KEY, automationMapper.ctx.getVars());
151        // Session injection
152        engine.put("Session", automationMapper.ctx.getCoreSession());
153        // User injection
154        PrincipalWrapper principalWrapper = new PrincipalWrapper((NuxeoPrincipal) automationMapper.ctx.getPrincipal());
155        engine.put("CurrentUser", principalWrapper);
156        engine.put("currentUser", principalWrapper);
157        // Env Properties injection
158        engine.put("Env", Framework.getProperties());
159        // DateWrapper injection
160        engine.put("CurrentDate", new DateWrapper());
161        // Workflow variables injection
162        if (automationMapper.ctx.get(Constants.VAR_WORKFLOW) != null) {
163            engine.put(Constants.VAR_WORKFLOW, automationMapper.ctx.get(Constants.VAR_WORKFLOW));
164        }
165        if (automationMapper.ctx.get(Constants.VAR_WORKFLOW_NODE) != null) {
166            engine.put(Constants.VAR_WORKFLOW_NODE, automationMapper.ctx.get(Constants.VAR_WORKFLOW_NODE));
167        }
168
169        // Helpers injection
170        ContextService contextService = Framework.getService(ContextService.class);
171        Map<String, ContextHelper> helperFunctions = contextService.getHelperFunctions();
172        for (String helperFunctionsId : helperFunctions.keySet()) {
173            engine.put(helperFunctionsId, helperFunctions.get(helperFunctionsId));
174        }
175        engine.eval(script);
176    }
177
178    @Override
179    public <T> T getInterface(Class<T> scriptingOperationInterface, String script, CoreSession session)
180            throws ScriptException, OperationException {
181        run(script, session);
182        Invocable inv = (Invocable) engines.get();
183        return inv.getInterface(scriptingOperationInterface);
184    }
185
186    protected void parseAutomationIDSForScripting(Map<String, List<String>> opMap, List<String> flatOps, String id) {
187        if (id.split("\\.").length > 2) {
188            return;
189        }
190        int idx = id.indexOf(".");
191        if (idx > 0) {
192            String obName = id.substring(0, idx);
193            List<String> ops = opMap.get(obName);
194            if (ops == null) {
195                ops = new ArrayList<>();
196            }
197            ops.add(id);
198            opMap.put(obName, ops);
199        } else {
200            // Flat operation: no need of category
201            flatOps.add(id);
202        }
203    }
204
205    protected void generateFunction(StringBuffer sb, String opId) {
206        sb.append("\n" + replaceDashByUnderscore(opId) + " = function(input,params) {");
207        sb.append("\nreturn automation.executeOperation('" + opId + "', input , params);");
208        sb.append("\n};");
209    }
210
211    protected void generateFlatFunction(StringBuffer sb, String opId) {
212        sb.append("\nvar " + replaceDashByUnderscore(opId) + " = function(input,params) {");
213        sb.append("\nreturn automation.executeOperation('" + opId + "', input , params);");
214        sb.append("\n};");
215    }
216
217    /**
218     * Prevents dashes in operation/chain ids. Only used to avoid javascript issues.
219     *
220     * @since 7.3
221     */
222    public static String replaceDashByUnderscore(String id) {
223        return id.replaceAll("[\\s\\-()]", "_");
224    }
225
226}