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