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(),
124                             Stream.concat(bindings.keySet().stream(), ctx.keySet().stream()))
125                     .distinct()
126                     .collect(Collectors.counting())
127                     .intValue();
128    }
129
130    @Override
131    public boolean isEmpty() {
132        return false;
133    }
134
135    @Override
136    public boolean containsKey(Object key) {
137        return automatic.containsKey(key) || bindings.containsKey(key) || ctx.containsKey(key);
138    }
139
140    @Override
141    public boolean containsValue(Object value) {
142        return automatic.containsValue(value) || bindings.containsValue(value) || ctx.containsValue(value);
143    }
144
145    @Override
146    public Object get(Object key) {
147        return automatic.getOrDefault(key,
148                () -> bindings.computeIfAbsent((String) key, k -> wrap(ctx.get(k))))
149                        .get();
150    }
151
152    @Override
153    public Object put(String key, Object value) {
154        bindings.put(key, value);
155        wrapped.put(key, value);
156        return value;
157    }
158
159    @Override
160    public Object remove(Object key) {
161        Object wrapped = bindings.remove(key);
162        Object unwrapped = ctx.remove(key);
163        if (wrapped == null) {
164            wrapped = wrap(unwrapped);
165        }
166        return wrapped;
167    }
168
169    @Override
170    public void putAll(Map<? extends String, ? extends Object> m) {
171        bindings.putAll(m);
172        wrapped.putAll(m);
173    }
174
175    @Override
176    public void clear() {
177        bindings.clear();
178        wrapped.clear();
179        ctx.clear();
180    }
181
182    @Override
183    public Set<String> keySet() {
184        throw new UnsupportedOperationException();
185    }
186
187    @Override
188    public Collection<Object> values() {
189        throw new UnsupportedOperationException();
190    }
191
192    @Override
193    public Set<java.util.Map.Entry<String, Object>> entrySet() {
194        throw new UnsupportedOperationException();
195    }
196
197    @Override
198    public Object getOrDefault(Object key, Object defaultValue) {
199        return Optional.ofNullable(get(key)).orElse(defaultValue);
200    }
201
202    @Override
203    public void forEach(BiConsumer<? super String, ? super Object> action) {
204        throw new UnsupportedOperationException();
205    }
206
207    @Override
208    public void replaceAll(BiFunction<? super String, ? super Object, ? extends Object> function) {
209        throw new UnsupportedOperationException();
210    }
211
212    @Override
213    public Object putIfAbsent(String key, Object value) {
214        throw new UnsupportedOperationException();
215    }
216
217    @Override
218    public boolean remove(Object key, Object value) {
219        throw new UnsupportedOperationException();
220    }
221
222    @Override
223    public boolean replace(String key, Object oldValue, Object newValue) {
224        throw new UnsupportedOperationException();
225    }
226
227    @Override
228    public Object replace(String key, Object value) {
229        throw new UnsupportedOperationException();
230    }
231
232    @Override
233    public Object computeIfAbsent(String key, Function<? super String, ? extends Object> mappingFunction) {
234        throw new UnsupportedOperationException();
235    }
236
237    @Override
238    public Object computeIfPresent(String key,
239            BiFunction<? super String, ? super Object, ? extends Object> remappingFunction) {
240        throw new UnsupportedOperationException();
241    }
242
243    @Override
244    public Object compute(String key, BiFunction<? super String, ? super Object, ? extends Object> remappingFunction) {
245        throw new UnsupportedOperationException();
246    }
247
248    @Override
249    public Object merge(String key, Object value,
250            BiFunction<? super Object, ? super Object, ? extends Object> remappingFunction) {
251        throw new UnsupportedOperationException();
252    }
253
254    public static class ScriptBuilder {
255
256        public CompiledScript build(Compilable compilable) throws ScriptException {
257            return compilable.compile(source());
258        }
259
260        public String source() {
261            StringBuilder sb = new StringBuilder();
262            AutomationService as = Framework.getService(AutomationService.class);
263            Map<String, List<String>> opMap = new HashMap<>();
264            List<String> flatOps = new ArrayList<>();
265            List<String> ids = new ArrayList<>();
266            for (OperationType op : as.getOperations()) {
267                ids.add(op.getId());
268                if (op.getAliases() != null) {
269                    Collections.addAll(ids, op.getAliases());
270                }
271            }
272            // Create js object related to operation categories
273            for (String id : ids) {
274                parseAutomationIDSForScripting(opMap, flatOps, id);
275            }
276            for (String obName : opMap.keySet()) {
277                List<String> ops = opMap.get(obName);
278                sb.append("\nvar ").append(obName).append("={};");
279                for (String opId : ops) {
280                    generateFunction(sb, opId);
281                }
282            }
283            for (String opId : flatOps) {
284                generateFlatFunction(sb, opId);
285            }
286            return sb.toString();
287        }
288
289        protected void parseAutomationIDSForScripting(Map<String, List<String>> opMap, List<String> flatOps,
290                String id) {
291            if (id.split("\\.").length > 2) {
292                return;
293            }
294            int idx = id.indexOf(".");
295            if (idx > 0) {
296                String obName = id.substring(0, idx);
297                List<String> ops = opMap.get(obName);
298                if (ops == null) {
299                    ops = new ArrayList<>();
300                }
301                ops.add(id);
302                opMap.put(obName, ops);
303            } else {
304                // Flat operation: no need of category
305                flatOps.add(id);
306            }
307        }
308
309        protected void generateFunction(StringBuilder sb, String opId) {
310            sb.append("\n")
311              .append(replaceDashByUnderscore(opId))
312              .append(" = function(input,params) {")
313              .append("\nreturn automation.executeOperation('")
314              .append(opId)
315              .append("', input , params);")
316              .append("\n};");
317        }
318
319        protected void generateFlatFunction(StringBuilder sb, String opId) {
320            sb.append("\nvar ")
321              .append(replaceDashByUnderscore(opId))
322              .append(" = function(input,params) {")
323              .append("\nreturn automation.executeOperation('")
324              .append(opId)
325              .append("', input , params);")
326              .append("\n};");
327        }
328
329        protected String replaceDashByUnderscore(String id) {
330            return id.replaceAll("[\\s\\-()]", "_");
331        }
332
333    }
334
335}