/* * (C) Copyright 2013-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: * bstefanescu * vpasquier <vpasquier@nuxeo.com> * slacoin <slacoin@nuxeo.com> */ package org.nuxeo.ecm.automation; import java.security.Principal; import java.util.AbstractMap; import java.util.AbstractSet; import java.util.ArrayList; import java.util.Deque; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import org.nuxeo.ecm.automation.core.Constants; import org.nuxeo.ecm.automation.core.scripting.Expression; import org.nuxeo.ecm.automation.core.trace.TracerFactory; import org.nuxeo.ecm.core.api.CoreSession; import org.nuxeo.runtime.api.Framework; import org.nuxeo.runtime.transaction.TransactionHelper; /** * An operation context. Holds context objects, a context parameters map and a list of operations to run. * <p> * Context objects are: * <ul> * <li>The Operation Chain Input - optional. It will be used as the input for the first operation in the chain. If input * is null then only VOID methods in the first operation will be matched. * <li>A Core Session - which is optional and should be provided by the caller. (either at creation time as a * constructor argument, either using the {@link #setCoreSession(CoreSession)} method. When running the operation chain * in asynchronous mode another session will be created by preserving the current session credentials. * </ul> * <p> * Each entry in the operation list contains the ID of the operation to be run and a map of operation parameters to use * when initializing the operation. * <p> * The context parameters map can be filled with contextual information by the caller. Each operation will be able to * access the contextual data at runtime and to update it if needed. * * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a> */ public class OperationContext extends AbstractMap<String,Object> implements AutoCloseable { /** * Whether to save the session at the end of the chain execution. The default is true. */ protected boolean commit = true; protected final transient List<CleanupHandler> cleanupHandlers; protected final Map<String, Object> vars; /** * Each stack use a key the type of the objects in the stack: document, documents, blob or blobs */ protected final transient Map<String, Deque<Object>> stacks = new HashMap<>(); /** * A logins stack manage multiple logins and sessions in a single chain execution */ protected transient LoginStack loginStack; /** * The execution input that will be updated after an operation run with the operation output */ protected Object input; /** * A list of trace. Since 5.7.3 messages is no longer useful for tracing. Use chain call backs to do it. */ protected List<String> trace; /** * @since 5.7.3 Collect operation invokes. */ protected OperationCallback callback; public OperationContext() { this(null); } public OperationContext(CoreSession session) { this(session, new HashMap<>()); } protected OperationContext(CoreSession session, Map<String, Object> bindings) { vars = bindings; cleanupHandlers = new ArrayList<>(); loginStack = new LoginStack(session); trace = new ArrayList<>(); callback = Framework.getService(TracerFactory.class).newTracer(); } public void setCoreSession(CoreSession session) { loginStack.setSession(session); } public void setCommit(boolean commit) { this.commit = commit; } public boolean isCommit() { return commit; } public CoreSession getCoreSession() { return loginStack.getSession(); } public LoginStack getLoginStack() { return loginStack; } public Principal getPrincipal() { CoreSession session = loginStack.getSession(); return session != null ? session.getPrincipal() : null; } public void setInput(Object input) { this.input = input; } public Object getInput() { return input; } /** * Push the whole map into the context. * * @since 9.1 */ public void push(Map<String, ?> map) { map.forEach(this::push); } /** * Pop all entries from the context giving the provided map keys. * * @param map * * @since 9.1 */ public void pop(Map<String, ?> map) { map.forEach((k, v) -> pop(k)); } public Object push(String type, Object obj) { Deque<Object> stack = stacks.get(type); if (stack == null) { if (vars.containsKey(type)) { throw new IllegalStateException(type + " is not a stack"); } stack = new LinkedList<>(); stacks.put(type, stack); } Object current = stack.peek(); stack.push(obj); vars.put(type, obj); return current; } public Object peek(String type) { return vars.get(type); } public Object pop(String type) { Deque<Object> stack = stacks.get(type); if (stack == null) { return null; } vars.remove(type); Object obj = stack.pop(); if (stack.isEmpty()) { stacks.remove(type); } return obj; } public Object pull(String type) { Deque<Object> stack = stacks.get(type); if (stack == null) { return null; } Object obj = stack.removeLast(); if (stack.isEmpty()) { vars.remove(type); stacks.remove(type); } return obj; } public <T> T getAdapter(Class<T> type) { if (type.isAssignableFrom(getClass())) { return type.cast(this); } else if (type.isAssignableFrom(CoreSession.class)) { return type.cast(getCoreSession()); } else if (type.isAssignableFrom(Principal.class)) { return type.cast(getPrincipal()); } else { // try nuxeo services return Framework.getService(type); } } public void addCleanupHandler(CleanupHandler handler) { cleanupHandlers.add(handler); } public void removeCleanupHandler(CleanupHandler handler) { cleanupHandlers.remove(handler); } @Override public void close() throws OperationException { if (getCoreSession() != null && isCommit()) { // auto save session if any. getCoreSession().save(); } trace.clear(); loginStack.clear(); cleanupHandlers.forEach(CleanupHandler::cleanup); } /** * Set the rollback mark on the current tx. This will cause the transaction to rollback. Also this is setting the * session commit flag on false */ public void setRollback() { setCommit(false); TransactionHelper.setTransactionRollbackOnly(); } public Map<String, Object> getVars() { return vars; } /** the map API */ @Override public boolean containsKey(Object key) { if (Constants.VAR_RUNTIME_CHAIN.equals(key)) { return true; } return super.containsKey(key); } @Override public Object get(Object key) { if (Constants.VAR_RUNTIME_CHAIN.equals(key)) { return this; } return resolve(vars.get(key)); } @Override public Object put(String key, Object value) { if (Constants.VAR_RUNTIME_CHAIN.equals(key)) { throw new IllegalArgumentException(Constants.VAR_RUNTIME_CHAIN + " is reserved, not writable"); } return resolve(vars.put(key, value)); } @Override public Object remove(Object key) { if (Constants.VAR_RUNTIME_CHAIN.equals(key)) { throw new IllegalArgumentException(Constants.VAR_RUNTIME_CHAIN + " is reserved, not writable"); } return resolve(vars.remove(key)); } @Override public Set<Map.Entry<String, Object>> entrySet() { return new AbstractSet<Map.Entry<String,Object>>() { @Override public Iterator<Entry<String, Object>> iterator() { Iterator<Entry<String,Object>> iterator = vars.entrySet().iterator(); return new Iterator<Entry<String,Object>>() { @Override public boolean hasNext() { return iterator.hasNext(); } @Override public Entry<String, Object> next() { Entry<String,Object> entry = iterator.next(); return new Entry<String,Object>() { @Override public String getKey() { return entry.getKey(); } @Override public Object getValue() { return resolve(entry.getValue()); } @Override public Object setValue(Object value) { Object previous = entry.setValue(value); return resolve(previous); } }; } }; } @Override public int size() { return vars.size(); } }; } /** * @since 5.7.3 */ public OperationCallback getCallback() { return callback; } /** * @since 5.7.3 */ public void setCallback(OperationCallback chainCallback) { callback = chainCallback; } /** * @since 5.7.3 * @param isolate * define if keeps context variables for the subcontext * @param input * an input object * @return a subcontext */ public OperationContext getSubContext(boolean isolate, Object input) { Map<String, Object> vars = isolate ? new HashMap<>(getVars()) : getVars(); OperationContext subctx = new OperationContext(getCoreSession(), vars); subctx.setInput(input); subctx.setCallback(callback); return subctx; } /** * @since 9.1 * @param isolate * define if keeps context variables for the subcontext * @return a subcontext */ public OperationContext getSubContext(boolean isolate) { return getSubContext(isolate, getInput()); } /** * Evaluate the expression against this context if needed * @param obj * @return the resolved value * * @since 9.1 */ public Object resolve(Object obj) { if (!(obj instanceof Expression)) { return obj; } return ((Expression) obj).eval(this); } }