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.OperationContext;
039import org.nuxeo.ecm.automation.OperationException;
040import org.nuxeo.ecm.automation.OperationType;
041import org.nuxeo.ecm.automation.context.ContextHelper;
042import org.nuxeo.ecm.automation.context.ContextService;
043import org.nuxeo.ecm.automation.core.Constants;
044import org.nuxeo.ecm.automation.core.scripting.DateWrapper;
045import org.nuxeo.ecm.automation.core.scripting.DocumentWrapper;
046import org.nuxeo.ecm.automation.core.scripting.PrincipalWrapper;
047import org.nuxeo.ecm.core.api.CoreSession;
048import org.nuxeo.ecm.core.api.DocumentModel;
049import org.nuxeo.ecm.core.api.DocumentModelList;
050import org.nuxeo.ecm.core.api.NuxeoException;
051import org.nuxeo.ecm.core.api.NuxeoPrincipal;
052import org.nuxeo.runtime.api.Framework;
053
054/**
055 * @since 7.2
056 */
057public class AutomationScriptingServiceImpl implements AutomationScriptingService {
058
059    protected String jsWrapper = null;
060
061    protected OperationContext operationContext;
062
063    protected String getJSWrapper(boolean refresh) throws OperationException {
064        if (jsWrapper == null || refresh) {
065            StringBuffer sb = new StringBuffer();
066            AutomationService as = Framework.getService(AutomationService.class);
067            Map<String, List<String>> opMap = new HashMap<>();
068            List<String> flatOps = new ArrayList<>();
069            List<String> ids = new ArrayList<>();
070            for (OperationType op : as.getOperations()) {
071                ids.add(op.getId());
072                if (op.getAliases() != null) {
073                    Collections.addAll(ids, op.getAliases());
074                }
075            }
076            // Create js object related to operation categories
077            for (String id : ids) {
078                parseAutomationIDSForScripting(opMap, flatOps, id);
079            }
080            for (String obName : opMap.keySet()) {
081                List<String> ops = opMap.get(obName);
082                sb.append("\nvar ").append(obName).append("={};");
083                for (String opId : ops) {
084                    generateFunction(sb, opId);
085                }
086            }
087            for (String opId : flatOps) {
088                generateFlatFunction(sb, opId);
089            }
090            jsWrapper = sb.toString();
091        }
092        return jsWrapper;
093    }
094
095    @Override
096    public void setOperationContext(OperationContext ctx) {
097        this.operationContext = operationContexts.get();
098        this.operationContext = wrapContext(ctx);
099    }
100
101    protected OperationContext wrapContext(OperationContext ctx) {
102        for (String entryId : ctx.keySet()) {
103            Object entry = ctx.get(entryId);
104            if (entry instanceof DocumentModel) {
105                ctx.put(entryId, new DocumentWrapper(ctx.getCoreSession(), (DocumentModel) entry));
106            }
107            if (entry instanceof DocumentModelList) {
108                List<DocumentWrapper> docs = new ArrayList<>();
109                for (DocumentModel doc : (DocumentModelList) entry) {
110                    docs.add(new DocumentWrapper(ctx.getCoreSession(), doc));
111                }
112                ctx.put(entryId, docs);
113            }
114        }
115        return ctx;
116    }
117
118    @Override
119    public String getJSWrapper() throws OperationException {
120        return getJSWrapper(false);
121    }
122
123    protected final ThreadLocal<ScriptEngine> engines = new ThreadLocal<ScriptEngine>() {
124        @Override
125        protected ScriptEngine initialValue() {
126            return Framework.getService(ScriptEngineManager.class).getEngineByName(
127                    AutomationScriptingConstants.NX_NASHORN);
128        }
129    };
130
131    protected final ThreadLocal<OperationContext> operationContexts = new ThreadLocal<OperationContext>() {
132        @Override
133        protected OperationContext initialValue() {
134            return new OperationContext();
135        }
136    };
137
138    @Override
139    public void run(InputStream in, CoreSession session) throws ScriptException, OperationException {
140        try {
141            run(IOUtils.toString(in, "UTF-8"), session);
142        } catch (IOException e) {
143            throw new NuxeoException(e);
144        }
145    }
146
147    @Override
148    public void run(String script, CoreSession session) throws ScriptException, OperationException {
149        ScriptEngine engine = engines.get();
150        engine.setContext(new SimpleScriptContext());
151        engine.eval(getJSWrapper());
152
153        // Initialize Operation Context
154        if (operationContext == null) {
155            operationContext = operationContexts.get();
156            operationContext.setCoreSession(session);
157        }
158
159        // Injecting Automation Mapper 'automation'
160        AutomationMapper automationMapper = new AutomationMapper(session, operationContext);
161        engine.put(AutomationScriptingConstants.AUTOMATION_MAPPER_KEY, automationMapper);
162
163        // Inject operation context vars in 'Context'
164        engine.put(AutomationScriptingConstants.AUTOMATION_CTX_KEY, automationMapper.ctx.getVars());
165        // Session injection
166        engine.put("Session", automationMapper.ctx.getCoreSession());
167        // User injection
168        PrincipalWrapper principalWrapper = new PrincipalWrapper((NuxeoPrincipal) automationMapper.ctx.getPrincipal());
169        engine.put("CurrentUser", principalWrapper);
170        engine.put("currentUser", principalWrapper);
171        // Env Properties injection
172        engine.put("Env", Framework.getProperties());
173        // DateWrapper injection
174        engine.put("CurrentDate", new DateWrapper());
175        // Workflow variables injection
176        if (automationMapper.ctx.get(Constants.VAR_WORKFLOW) != null) {
177            engine.put(Constants.VAR_WORKFLOW, automationMapper.ctx.get(Constants.VAR_WORKFLOW));
178        }
179        if (automationMapper.ctx.get(Constants.VAR_WORKFLOW_NODE) != null) {
180            engine.put(Constants.VAR_WORKFLOW_NODE, automationMapper.ctx.get(Constants.VAR_WORKFLOW_NODE));
181        }
182
183        // Helpers injection
184        ContextService contextService = Framework.getService(ContextService.class);
185        Map<String, ContextHelper> helperFunctions = contextService.getHelperFunctions();
186        for(String helperFunctionsId: helperFunctions.keySet()){
187            engine.put(helperFunctionsId,helperFunctions.get(helperFunctionsId));
188        }
189
190        engine.eval(script);
191    }
192
193    @Override
194    public <T> T getInterface(Class<T> scriptingOperationInterface, String script, CoreSession session)
195            throws ScriptException, OperationException {
196        run(script, session);
197        Invocable inv = (Invocable) engines.get();
198        return inv.getInterface(scriptingOperationInterface);
199    }
200
201    protected void parseAutomationIDSForScripting(Map<String, List<String>> opMap, List<String> flatOps, String id) {
202        if (id.split("\\.").length > 2) {
203            return;
204        }
205        int idx = id.indexOf(".");
206        if (idx > 0) {
207            String obName = id.substring(0, idx);
208            List<String> ops = opMap.get(obName);
209            if (ops == null) {
210                ops = new ArrayList<>();
211            }
212            ops.add(id);
213            opMap.put(obName, ops);
214        } else {
215            // Flat operation: no need of category
216            flatOps.add(id);
217        }
218    }
219
220    protected void generateFunction(StringBuffer sb, String opId) {
221        sb.append("\n" + replaceDashByUnderscore(opId) + " = function(input,params) {");
222        sb.append("\nreturn automation.executeOperation('" + opId + "', input , params);");
223        sb.append("\n};");
224    }
225
226    protected void generateFlatFunction(StringBuffer sb, String opId) {
227        sb.append("\nvar " + replaceDashByUnderscore(opId) + " = function(input,params) {");
228        sb.append("\nreturn automation.executeOperation('" + opId + "', input , params);");
229        sb.append("\n};");
230    }
231
232    /**
233     * Prevents dashes in operation/chain ids. Only used to avoid javascript issues.
234     * @since 7.3
235     */
236    public static String replaceDashByUnderscore(String id) {
237        return id.replaceAll("[\\s\\-()]", "_");
238    }
239}