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