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 *     Thierry Delprat <tdelprat@nuxeo.com>
018 *     Vladimir Pasquier <vpasquier@nuxeo.com>
019 */
020package org.nuxeo.automation.scripting.internals;
021
022import java.util.ArrayList;
023import java.util.Collection;
024import java.util.Collections;
025import java.util.HashMap;
026import java.util.List;
027import java.util.Map;
028import java.util.Optional;
029import java.util.Set;
030import java.util.function.BiConsumer;
031import java.util.function.BiFunction;
032import java.util.function.Function;
033import java.util.function.Supplier;
034import java.util.stream.Collectors;
035import java.util.stream.Stream;
036
037import javax.script.Bindings;
038import javax.script.Compilable;
039import javax.script.CompiledScript;
040import javax.script.ScriptException;
041import javax.script.SimpleBindings;
042
043import org.nuxeo.automation.scripting.api.AutomationScriptingConstants;
044import org.nuxeo.ecm.automation.AutomationService;
045import org.nuxeo.ecm.automation.OperationContext;
046import org.nuxeo.ecm.automation.OperationParameters;
047import org.nuxeo.ecm.automation.OperationType;
048import org.nuxeo.ecm.automation.context.ContextHelper;
049import org.nuxeo.ecm.automation.context.ContextService;
050import org.nuxeo.ecm.automation.core.scripting.DateWrapper;
051import org.nuxeo.ecm.automation.core.scripting.PrincipalWrapper;
052import org.nuxeo.ecm.core.api.NuxeoException;
053import org.nuxeo.ecm.core.api.NuxeoPrincipal;
054import org.nuxeo.runtime.api.Framework;
055
056import jdk.nashorn.api.scripting.ScriptObjectMirror;
057
058/**
059 * Class injected/published in Nashorn engine to execute automation service.
060 *
061 * @since 7.2
062 */
063public class AutomationMapper implements Bindings {
064
065    protected final OperationContext ctx;
066
067    protected final Map<String, Supplier<Object>> automatic = new HashMap<>();
068
069    protected final Bindings bindings = new SimpleBindings();
070
071    protected final Map<String, Object> wrapped = new HashMap<>();
072
073    public static CompiledScript compile(Compilable compilable)  {
074        try {
075            return new ScriptBuilder().build(compilable);
076        } catch (ScriptException cause) {
077           throw new NuxeoException("Cannot compile mapper initialization script", cause);
078        }
079    }
080
081    public AutomationMapper(OperationContext ctx) {
082        this.ctx = ctx;
083        automatic.put("Session", () -> ctx.getCoreSession());
084        automatic.put(AutomationScriptingConstants.AUTOMATION_CTX_KEY, () -> ctx.getVars());
085        automatic.put(AutomationScriptingConstants.AUTOMATION_MAPPER_KEY, () -> this);
086        automatic.put("CurrentUser", () -> new PrincipalWrapper((NuxeoPrincipal) ctx.getPrincipal()));
087        automatic.put("currentUser", () -> new PrincipalWrapper((NuxeoPrincipal) ctx.getPrincipal()));
088        automatic.put("Env", () -> Framework.getProperties());
089        automatic.put("CurrentDate", () -> new DateWrapper());
090        // Helpers injection
091        ContextService contextService = Framework.getService(ContextService.class);
092        Map<String, ContextHelper> helperFunctions = contextService.getHelperFunctions();
093        for (String helperFunctionsId : helperFunctions.keySet()) {
094            automatic.put(helperFunctionsId, () -> helperFunctions.get(helperFunctionsId));
095        }
096    }
097
098    public void flush() {
099        wrapped.forEach((k, v) -> ctx.put(k, unwrap(v)));
100        wrapped.clear();
101    }
102
103    public Object unwrap(Object wrapped) {
104        return DocumentScriptingWrapper.unwrap(wrapped);
105    }
106
107    public Object wrap(Object unwrapped) {
108        return DocumentScriptingWrapper.wrap(unwrapped, this);
109    }
110
111    public Object executeOperation(String opId, Object input, ScriptObjectMirror parameters) throws Exception {
112        flush();
113        ctx.setInput(input = DocumentScriptingWrapper.unwrap(input));
114        AutomationService automation = Framework.getService(AutomationService.class);
115        Class<?> typeof = input == null ? Void.TYPE : input.getClass();
116        OperationParameters args = new OperationParameters(opId, DocumentScriptingWrapper.unwrap(parameters));
117        Object output = automation.compileChain(typeof, args).invoke(ctx);
118        return wrap(output);
119    }
120
121    @Override
122    public int size() {
123        return Stream
124                .concat(automatic.keySet().stream(), Stream.concat(bindings.keySet().stream(), ctx.keySet().stream()))
125                .distinct().collect(Collectors.counting()).intValue();
126    }
127
128    @Override
129    public boolean isEmpty() {
130        return false;
131    }
132
133    @Override
134    public boolean containsKey(Object key) {
135        return automatic.containsKey(key) || bindings.containsKey(key) || ctx.containsKey(key);
136    }
137
138    @Override
139    public boolean containsValue(Object value) {
140        return automatic.containsValue(value) || bindings.containsValue(value) || ctx.containsValue(value);
141    }
142
143    @Override
144    public Object get(Object key) {
145        return automatic.getOrDefault(key,
146                () -> bindings.computeIfAbsent((String) key, k -> wrap(ctx.get(k))))
147                .get();
148    }
149
150    @Override
151    public Object put(String key, Object value) {
152        bindings.put(key, value);
153        wrapped.put(key, value);
154        return value;
155    }
156
157    @Override
158    public Object remove(Object key) {
159        Object wrapped = bindings.remove(key);
160        Object unwrapped = ctx.remove(key);
161        if (wrapped == null) {
162            wrapped = wrap(unwrapped);
163        }
164        return wrapped;
165    }
166
167    @Override
168    public void putAll(Map<? extends String, ? extends Object> m) {
169        bindings.putAll(m);
170        wrapped.putAll(m);
171    }
172
173    @Override
174    public void clear() {
175        bindings.clear();
176        wrapped.clear();
177        ctx.clear();
178    }
179
180    @Override
181    public Set<String> keySet() {
182        throw new UnsupportedOperationException();
183    }
184
185    @Override
186    public Collection<Object> values() {
187        throw new UnsupportedOperationException();
188    }
189
190    @Override
191    public Set<java.util.Map.Entry<String, Object>> entrySet() {
192        throw new UnsupportedOperationException();
193    }
194
195    @Override
196    public Object getOrDefault(Object key, Object defaultValue) {
197        return Optional.ofNullable(get(key)).orElse(defaultValue);
198    }
199
200    @Override
201    public void forEach(BiConsumer<? super String, ? super Object> action) {
202        throw new UnsupportedOperationException();
203    }
204
205    @Override
206    public void replaceAll(BiFunction<? super String, ? super Object, ? extends Object> function) {
207        throw new UnsupportedOperationException();
208    }
209
210    @Override
211    public Object putIfAbsent(String key, Object value) {
212        throw new UnsupportedOperationException();
213    }
214
215    @Override
216    public boolean remove(Object key, Object value) {
217        throw new UnsupportedOperationException();
218    }
219
220    @Override
221    public boolean replace(String key, Object oldValue, Object newValue) {
222        throw new UnsupportedOperationException();
223    }
224
225    @Override
226    public Object replace(String key, Object value) {
227        throw new UnsupportedOperationException();
228    }
229
230    @Override
231    public Object computeIfAbsent(String key, Function<? super String, ? extends Object> mappingFunction) {
232        throw new UnsupportedOperationException();
233    }
234
235    @Override
236    public Object computeIfPresent(String key,
237            BiFunction<? super String, ? super Object, ? extends Object> remappingFunction) {
238        throw new UnsupportedOperationException();
239    }
240
241    @Override
242    public Object compute(String key, BiFunction<? super String, ? super Object, ? extends Object> remappingFunction) {
243        throw new UnsupportedOperationException();
244    }
245
246    @Override
247    public Object merge(String key, Object value,
248            BiFunction<? super Object, ? super Object, ? extends Object> remappingFunction) {
249        throw new UnsupportedOperationException();
250    }
251
252    public static class ScriptBuilder {
253
254        public CompiledScript build(Compilable compilable) throws ScriptException {
255            return compilable.compile(source());
256        }
257
258        public String source() {
259            StringBuffer sb = new StringBuffer();
260            AutomationService as = Framework.getService(AutomationService.class);
261            Map<String, List<String>> opMap = new HashMap<>();
262            List<String> flatOps = new ArrayList<>();
263            List<String> ids = new ArrayList<>();
264            for (OperationType op : as.getOperations()) {
265                ids.add(op.getId());
266                if (op.getAliases() != null) {
267                    Collections.addAll(ids, op.getAliases());
268                }
269            }
270            // Create js object related to operation categories
271            for (String id : ids) {
272                parseAutomationIDSForScripting(opMap, flatOps, id);
273            }
274            for (String obName : opMap.keySet()) {
275                List<String> ops = opMap.get(obName);
276                sb.append("\nvar ").append(obName).append("={};");
277                for (String opId : ops) {
278                    generateFunction(sb, opId);
279                }
280            }
281            for (String opId : flatOps) {
282                generateFlatFunction(sb, opId);
283            }
284            return sb.toString();
285        }
286
287        protected void parseAutomationIDSForScripting(Map<String, List<String>> opMap, List<String> flatOps,
288                String id) {
289            if (id.split("\\.").length > 2) {
290                return;
291            }
292            int idx = id.indexOf(".");
293            if (idx > 0) {
294                String obName = id.substring(0, idx);
295                List<String> ops = opMap.get(obName);
296                if (ops == null) {
297                    ops = new ArrayList<>();
298                }
299                ops.add(id);
300                opMap.put(obName, ops);
301            } else {
302                // Flat operation: no need of category
303                flatOps.add(id);
304            }
305        }
306
307        protected void generateFunction(StringBuffer sb, String opId) {
308            sb.append("\n" + replaceDashByUnderscore(opId) + " = function(input,params) {");
309            sb.append("\nreturn automation.executeOperation('" + opId + "', input , params);");
310            sb.append("\n};");
311        }
312
313        protected void generateFlatFunction(StringBuffer sb, String opId) {
314            sb.append("\nvar " + replaceDashByUnderscore(opId) + " = function(input,params) {");
315            sb.append("\nreturn automation.executeOperation('" + opId + "', input , params);");
316            sb.append("\n};");
317        }
318
319        protected String replaceDashByUnderscore(String id) {
320            return id.replaceAll("[\\s\\-()]", "_");
321        }
322
323    }
324
325}