/*
* (C) Copyright 2015-2016 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Contributors:
* Thierry Delprat <tdelprat@nuxeo.com>
* Vladimir Pasquier <vpasquier@nuxeo.com>
*/
package org.nuxeo.automation.scripting.internals;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.script.Bindings;
import javax.script.Compilable;
import javax.script.CompiledScript;
import javax.script.ScriptException;
import javax.script.SimpleBindings;
import org.nuxeo.automation.scripting.api.AutomationScriptingConstants;
import org.nuxeo.ecm.automation.AutomationService;
import org.nuxeo.ecm.automation.OperationContext;
import org.nuxeo.ecm.automation.OperationParameters;
import org.nuxeo.ecm.automation.OperationType;
import org.nuxeo.ecm.automation.context.ContextHelper;
import org.nuxeo.ecm.automation.context.ContextService;
import org.nuxeo.ecm.automation.core.scripting.DateWrapper;
import org.nuxeo.ecm.automation.core.scripting.PrincipalWrapper;
import org.nuxeo.ecm.core.api.NuxeoException;
import org.nuxeo.ecm.core.api.NuxeoPrincipal;
import org.nuxeo.runtime.api.Framework;
import jdk.nashorn.api.scripting.ScriptObjectMirror;
/**
* Class injected/published in Nashorn engine to execute automation service.
*
* @since 7.2
*/
public class AutomationMapper implements Bindings {
protected final OperationContext ctx;
protected final Map<String, Supplier<Object>> automatic = new HashMap<>();
protected final Bindings bindings = new SimpleBindings();
protected final Map<String, Object> wrapped = new HashMap<>();
public static CompiledScript compile(Compilable compilable) {
try {
return new ScriptBuilder().build(compilable);
} catch (ScriptException cause) {
throw new NuxeoException("Cannot compile mapper initialization script", cause);
}
}
public AutomationMapper(OperationContext ctx) {
this.ctx = ctx;
automatic.put("Session", () -> ctx.getCoreSession());
automatic.put(AutomationScriptingConstants.AUTOMATION_CTX_KEY, () -> ctx.getVars());
automatic.put(AutomationScriptingConstants.AUTOMATION_MAPPER_KEY, () -> this);
automatic.put("CurrentUser", () -> new PrincipalWrapper((NuxeoPrincipal) ctx.getPrincipal()));
automatic.put("currentUser", () -> new PrincipalWrapper((NuxeoPrincipal) ctx.getPrincipal()));
automatic.put("Env", () -> Framework.getProperties());
automatic.put("CurrentDate", () -> new DateWrapper());
// Helpers injection
ContextService contextService = Framework.getService(ContextService.class);
Map<String, ContextHelper> helperFunctions = contextService.getHelperFunctions();
for (String helperFunctionsId : helperFunctions.keySet()) {
automatic.put(helperFunctionsId, () -> helperFunctions.get(helperFunctionsId));
}
}
public void flush() {
wrapped.forEach((k, v) -> ctx.put(k, unwrap(v)));
wrapped.clear();
}
public Object unwrap(Object wrapped) {
return DocumentScriptingWrapper.unwrap(wrapped);
}
public Object wrap(Object unwrapped) {
return DocumentScriptingWrapper.wrap(unwrapped, this);
}
public Object executeOperation(String opId, Object input, ScriptObjectMirror parameters) throws Exception {
flush();
ctx.setInput(input = DocumentScriptingWrapper.unwrap(input));
AutomationService automation = Framework.getService(AutomationService.class);
Class<?> typeof = input == null ? Void.TYPE : input.getClass();
OperationParameters args = new OperationParameters(opId, DocumentScriptingWrapper.unwrap(parameters));
Object output = automation.compileChain(typeof, args).invoke(ctx);
return wrap(output);
}
@Override
public int size() {
return Stream
.concat(automatic.keySet().stream(), Stream.concat(bindings.keySet().stream(), ctx.keySet().stream()))
.distinct().collect(Collectors.counting()).intValue();
}
@Override
public boolean isEmpty() {
return false;
}
@Override
public boolean containsKey(Object key) {
return automatic.containsKey(key) || bindings.containsKey(key) || ctx.containsKey(key);
}
@Override
public boolean containsValue(Object value) {
return automatic.containsValue(value) || bindings.containsValue(value) || ctx.containsValue(value);
}
@Override
public Object get(Object key) {
return automatic.getOrDefault(key,
() -> bindings.computeIfAbsent((String) key, k -> wrap(ctx.get(k))))
.get();
}
@Override
public Object put(String key, Object value) {
bindings.put(key, value);
wrapped.put(key, value);
return value;
}
@Override
public Object remove(Object key) {
Object wrapped = bindings.remove(key);
Object unwrapped = ctx.remove(key);
if (wrapped == null) {
wrapped = wrap(unwrapped);
}
return wrapped;
}
@Override
public void putAll(Map<? extends String, ? extends Object> m) {
bindings.putAll(m);
wrapped.putAll(m);
}
@Override
public void clear() {
bindings.clear();
wrapped.clear();
ctx.clear();
}
@Override
public Set<String> keySet() {
throw new UnsupportedOperationException();
}
@Override
public Collection<Object> values() {
throw new UnsupportedOperationException();
}
@Override
public Set<java.util.Map.Entry<String, Object>> entrySet() {
throw new UnsupportedOperationException();
}
@Override
public Object getOrDefault(Object key, Object defaultValue) {
return Optional.ofNullable(get(key)).orElse(defaultValue);
}
@Override
public void forEach(BiConsumer<? super String, ? super Object> action) {
throw new UnsupportedOperationException();
}
@Override
public void replaceAll(BiFunction<? super String, ? super Object, ? extends Object> function) {
throw new UnsupportedOperationException();
}
@Override
public Object putIfAbsent(String key, Object value) {
throw new UnsupportedOperationException();
}
@Override
public boolean remove(Object key, Object value) {
throw new UnsupportedOperationException();
}
@Override
public boolean replace(String key, Object oldValue, Object newValue) {
throw new UnsupportedOperationException();
}
@Override
public Object replace(String key, Object value) {
throw new UnsupportedOperationException();
}
@Override
public Object computeIfAbsent(String key, Function<? super String, ? extends Object> mappingFunction) {
throw new UnsupportedOperationException();
}
@Override
public Object computeIfPresent(String key,
BiFunction<? super String, ? super Object, ? extends Object> remappingFunction) {
throw new UnsupportedOperationException();
}
@Override
public Object compute(String key, BiFunction<? super String, ? super Object, ? extends Object> remappingFunction) {
throw new UnsupportedOperationException();
}
@Override
public Object merge(String key, Object value,
BiFunction<? super Object, ? super Object, ? extends Object> remappingFunction) {
throw new UnsupportedOperationException();
}
public static class ScriptBuilder {
public CompiledScript build(Compilable compilable) throws ScriptException {
return compilable.compile(source());
}
public String source() {
StringBuffer sb = new StringBuffer();
AutomationService as = Framework.getService(AutomationService.class);
Map<String, List<String>> opMap = new HashMap<>();
List<String> flatOps = new ArrayList<>();
List<String> ids = new ArrayList<>();
for (OperationType op : as.getOperations()) {
ids.add(op.getId());
if (op.getAliases() != null) {
Collections.addAll(ids, op.getAliases());
}
}
// Create js object related to operation categories
for (String id : ids) {
parseAutomationIDSForScripting(opMap, flatOps, id);
}
for (String obName : opMap.keySet()) {
List<String> ops = opMap.get(obName);
sb.append("\nvar ").append(obName).append("={};");
for (String opId : ops) {
generateFunction(sb, opId);
}
}
for (String opId : flatOps) {
generateFlatFunction(sb, opId);
}
return sb.toString();
}
protected void parseAutomationIDSForScripting(Map<String, List<String>> opMap, List<String> flatOps,
String id) {
if (id.split("\\.").length > 2) {
return;
}
int idx = id.indexOf(".");
if (idx > 0) {
String obName = id.substring(0, idx);
List<String> ops = opMap.get(obName);
if (ops == null) {
ops = new ArrayList<>();
}
ops.add(id);
opMap.put(obName, ops);
} else {
// Flat operation: no need of category
flatOps.add(id);
}
}
protected void generateFunction(StringBuffer sb, String opId) {
sb.append("\n" + replaceDashByUnderscore(opId) + " = function(input,params) {");
sb.append("\nreturn automation.executeOperation('" + opId + "', input , params);");
sb.append("\n};");
}
protected void generateFlatFunction(StringBuffer sb, String opId) {
sb.append("\nvar " + replaceDashByUnderscore(opId) + " = function(input,params) {");
sb.append("\nreturn automation.executeOperation('" + opId + "', input , params);");
sb.append("\n};");
}
protected String replaceDashByUnderscore(String id) {
return id.replaceAll("[\\s\\-()]", "_");
}
}
}