001/*
002 * (C) Copyright 2015 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.DocumentWrapper;
047import org.nuxeo.ecm.automation.core.scripting.PrincipalWrapper;
048import org.nuxeo.ecm.core.api.CoreSession;
049import org.nuxeo.ecm.core.api.DocumentModel;
050import org.nuxeo.ecm.core.api.DocumentModelList;
051import org.nuxeo.ecm.core.api.NuxeoException;
052import org.nuxeo.ecm.core.api.NuxeoPrincipal;
053import org.nuxeo.runtime.api.Framework;
054
055/**
056 * @since 7.2
057 */
058public class AutomationScriptingServiceImpl implements AutomationScriptingService {
059
060    protected String jsWrapper = null;
061
062    protected ScriptOperationContext operationContext;
063
064    protected String getJSWrapper(boolean refresh) throws OperationException {
065        if (jsWrapper == null || refresh) {
066            StringBuffer sb = new StringBuffer();
067            AutomationService as = Framework.getService(AutomationService.class);
068            Map<String, List<String>> opMap = new HashMap<>();
069            List<String> flatOps = new ArrayList<>();
070            List<String> ids = new ArrayList<>();
071            for (OperationType op : as.getOperations()) {
072                ids.add(op.getId());
073                if (op.getAliases() != null) {
074                    Collections.addAll(ids, op.getAliases());
075                }
076            }
077            // Create js object related to operation categories
078            for (String id : ids) {
079                parseAutomationIDSForScripting(opMap, flatOps, id);
080            }
081            for (String obName : opMap.keySet()) {
082                List<String> ops = opMap.get(obName);
083                sb.append("\nvar ").append(obName).append("={};");
084                for (String opId : ops) {
085                    generateFunction(sb, opId);
086                }
087            }
088            for (String opId : flatOps) {
089                generateFlatFunction(sb, opId);
090            }
091            jsWrapper = sb.toString();
092        }
093        return jsWrapper;
094    }
095
096    @Override
097    public void setOperationContext(ScriptOperationContext ctx) {
098        this.operationContext = operationContexts.get();
099        this.operationContext = wrapContext(ctx);
100    }
101
102    protected ScriptOperationContext wrapContext(ScriptOperationContext ctx) {
103        for (String entryId : ctx.keySet()) {
104            Object entry = ctx.get(entryId);
105            if (entry instanceof DocumentModel) {
106                ctx.put(entryId, new DocumentWrapper(ctx.getCoreSession(), (DocumentModel) entry));
107            }
108            if (entry instanceof DocumentModelList) {
109                List<DocumentWrapper> docs = new ArrayList<>();
110                for (DocumentModel doc : (DocumentModelList) entry) {
111                    docs.add(new DocumentWrapper(ctx.getCoreSession(), doc));
112                }
113                ctx.put(entryId, docs);
114            }
115        }
116        return ctx;
117    }
118
119    @Override
120    public String getJSWrapper() throws OperationException {
121        return getJSWrapper(false);
122    }
123
124    protected final ThreadLocal<ScriptEngine> engines = new ThreadLocal<ScriptEngine>() {
125        @Override
126        protected ScriptEngine initialValue() {
127            return Framework.getService(ScriptEngineManager.class).getEngineByName(
128                    AutomationScriptingConstants.NX_NASHORN);
129        }
130    };
131
132    protected final ThreadLocal<ScriptOperationContext> operationContexts = new ThreadLocal<ScriptOperationContext>() {
133        @Override
134        protected ScriptOperationContext initialValue() {
135            return new ScriptOperationContext();
136        }
137    };
138
139    @Override
140    public void run(InputStream in, CoreSession session) throws ScriptException, OperationException {
141        try {
142            run(IOUtils.toString(in, "UTF-8"), session);
143        } catch (IOException e) {
144            throw new NuxeoException(e);
145        }
146    }
147
148    @Override
149    public void run(String script, CoreSession session) throws ScriptException, OperationException {
150        ScriptEngine engine = engines.get();
151        engine.setContext(new SimpleScriptContext());
152        engine.eval(getJSWrapper());
153
154        // Initialize Operation Context
155        if (operationContext == null) {
156            operationContext = operationContexts.get();
157            operationContext.setCoreSession(session);
158        }
159
160        // Injecting Automation Mapper 'automation'
161        AutomationMapper automationMapper = new AutomationMapper(session, operationContext);
162        engine.put(AutomationScriptingConstants.AUTOMATION_MAPPER_KEY, automationMapper);
163
164        // Inject operation context vars in 'Context'
165        engine.put(AutomationScriptingConstants.AUTOMATION_CTX_KEY, automationMapper.ctx.getVars());
166        // Session injection
167        engine.put("Session", automationMapper.ctx.getCoreSession());
168        // User injection
169        PrincipalWrapper principalWrapper = new PrincipalWrapper((NuxeoPrincipal) automationMapper.ctx.getPrincipal());
170        engine.put("CurrentUser", principalWrapper);
171        engine.put("currentUser", principalWrapper);
172        // Env Properties injection
173        engine.put("Env", Framework.getProperties());
174        // DateWrapper injection
175        engine.put("CurrentDate", new DateWrapper());
176        // Workflow variables injection
177        if (automationMapper.ctx.get(Constants.VAR_WORKFLOW) != null) {
178            engine.put(Constants.VAR_WORKFLOW, automationMapper.ctx.get(Constants.VAR_WORKFLOW));
179        }
180        if (automationMapper.ctx.get(Constants.VAR_WORKFLOW_NODE) != null) {
181            engine.put(Constants.VAR_WORKFLOW_NODE, automationMapper.ctx.get(Constants.VAR_WORKFLOW_NODE));
182        }
183
184        // Helpers injection
185        ContextService contextService = Framework.getService(ContextService.class);
186        Map<String, ContextHelper> helperFunctions = contextService.getHelperFunctions();
187        for (String helperFunctionsId : helperFunctions.keySet()) {
188            engine.put(helperFunctionsId, helperFunctions.get(helperFunctionsId));
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}