001/*
002 * (C) Copyright 2013-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 *     bstefanescu
018 *     vpasquier <vpasquier@nuxeo.com>
019 *     slacoin <slacoin@nuxeo.com>
020 */
021package org.nuxeo.ecm.automation;
022
023import java.util.AbstractMap;
024import java.util.AbstractSet;
025import java.util.ArrayList;
026import java.util.Deque;
027import java.util.HashMap;
028import java.util.Iterator;
029import java.util.LinkedList;
030import java.util.List;
031import java.util.Map;
032import java.util.Optional;
033import java.util.Set;
034import java.util.concurrent.Callable;
035import java.util.function.Consumer;
036
037import org.nuxeo.ecm.automation.core.Constants;
038import org.nuxeo.ecm.automation.core.scripting.Expression;
039import org.nuxeo.ecm.automation.core.trace.TracerFactory;
040import org.nuxeo.ecm.core.api.CoreSession;
041import org.nuxeo.ecm.core.api.NuxeoException;
042import org.nuxeo.ecm.core.api.NuxeoPrincipal;
043import org.nuxeo.runtime.api.Framework;
044import org.nuxeo.runtime.transaction.TransactionHelper;
045
046/**
047 * An operation context. Holds context objects, a context parameters map and a list of operations to run.
048 * <p>
049 * Context objects are:
050 * <ul>
051 * <li>The Operation Chain Input - optional. It will be used as the input for the first operation in the chain. If input
052 * is null then only VOID methods in the first operation will be matched.
053 * <li>A Core Session - which is optional and should be provided by the caller. (either at creation time as a
054 * constructor argument, either using the {@link #setCoreSession(CoreSession)} method. When running the operation chain
055 * in asynchronous mode another session will be created by preserving the current session credentials.
056 * </ul>
057 * <p>
058 * Each entry in the operation list contains the ID of the operation to be run and a map of operation parameters to use
059 * when initializing the operation.
060 * <p>
061 * The context parameters map can be filled with contextual information by the caller. Each operation will be able to
062 * access the contextual data at runtime and to update it if needed.
063 *
064 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
065 */
066public class OperationContext extends AbstractMap<String, Object> implements AutoCloseable {
067
068    /**
069     * Whether to save the session at the end of the chain execution. The default is true.
070     */
071    protected boolean commit = true;
072
073    protected final List<CleanupHandler> cleanupHandlers;
074
075    /**
076     * The context variables.
077     */
078    protected final Map<String, Object> vars;
079
080    /**
081     * Each stack use a key the type of the objects in the stack: document, documents, blob or blobs
082     */
083    protected final Map<String, Deque<Object>> stacks = new HashMap<>();
084
085    /**
086     * A logins stack manage multiple logins and sessions in a single chain execution
087     */
088    protected LoginStack loginStack;
089
090    /**
091     * The execution input that will be updated after an operation run with the operation output
092     */
093    protected Object input;
094
095    /**
096     * A list of trace. Since 5.7.3 messages is no longer useful for tracing. Use chain call backs to do it.
097     */
098    protected List<String> trace;
099
100    /**
101     * @since 5.7.3 Collect operation invokes.
102     */
103    protected OperationCallback callback;
104
105    public OperationContext() {
106        this(null);
107    }
108
109    public OperationContext(CoreSession session) {
110        this(session, new HashMap<>());
111    }
112
113    protected OperationContext(CoreSession session, Map<String, Object> bindings) {
114        vars = bindings;
115        cleanupHandlers = new ArrayList<>();
116        loginStack = new LoginStack(session);
117        trace = new ArrayList<>();
118        callback = Framework.getService(TracerFactory.class).newTracer();
119    }
120
121    public void setCoreSession(CoreSession session) {
122        loginStack.setSession(session);
123    }
124
125    public void setCommit(boolean commit) {
126        this.commit = commit;
127    }
128
129    public boolean isCommit() {
130        return commit && !TransactionHelper.isTransactionMarkedRollback();
131    }
132
133    public CoreSession getCoreSession() {
134        return loginStack.getSession();
135    }
136
137    public LoginStack getLoginStack() {
138        return loginStack;
139    }
140
141    public NuxeoPrincipal getPrincipal() {
142        CoreSession session = loginStack.getSession();
143        return session != null ? session.getPrincipal() : null;
144    }
145
146    public void setInput(Object input) {
147        this.input = input;
148    }
149
150    public Object getInput() {
151        return input;
152    }
153
154    public void push(String type, Object obj) {
155        stacks.computeIfAbsent(type, key -> new LinkedList<>()).push(obj);
156    }
157
158    public Object peek(String type) {
159        return Optional.ofNullable(stacks.get(type)).map(Deque::peek).orElse(null);
160    }
161
162    public Object pop(String type) {
163        return Optional.ofNullable(stacks.get(type)).map(stack -> {
164            Object obj = stack.pop();
165            if (stack.isEmpty()) {
166                stacks.remove(type);
167            }
168            return obj;
169        }).orElse(null);
170    }
171
172    public Object pull(String type) {
173        return Optional.ofNullable(stacks.get(type)).map(stack -> {
174            Object obj = stack.removeLast();
175            if (stack.isEmpty()) {
176                stacks.remove(type);
177            }
178            return obj;
179        }).orElse(null);
180    }
181
182    public <T> T getAdapter(Class<T> type) {
183        if (type.isAssignableFrom(getClass())) {
184            return type.cast(this);
185        } else if (type.isAssignableFrom(CoreSession.class)) {
186            return type.cast(getCoreSession());
187        } else if (type.isAssignableFrom(NuxeoPrincipal.class)) {
188            return type.cast(getPrincipal());
189        } else { // try nuxeo services
190            return Framework.getService(type);
191        }
192    }
193
194    public void addCleanupHandler(CleanupHandler handler) {
195        cleanupHandlers.add(handler);
196    }
197
198    public void removeCleanupHandler(CleanupHandler handler) {
199        cleanupHandlers.remove(handler);
200    }
201
202    @Override
203    public void close() {
204        if (getCoreSession() != null && isCommit()) {
205            // auto save session if any.
206            getCoreSession().save();
207        }
208        trace.clear();
209        loginStack.clear();
210        cleanupHandlers.forEach(CleanupHandler::cleanup);
211    }
212
213    /**
214     * Set the rollback mark on the current tx. This will cause the transaction to rollback. Also this is setting the
215     * session commit flag on false
216     */
217    public void setRollback() {
218        setCommit(false);
219        TransactionHelper.setTransactionRollbackOnly();
220    }
221
222    public Map<String, Object> getVars() {
223        return vars;
224    }
225
226    /** the map API */
227
228    @Override
229    public Object get(Object key) {
230        return resolve(vars.get(key));
231    }
232
233    @Override
234    public Object put(String key, Object value) {
235        if (Constants.VAR_RUNTIME_CHAIN.equals(key)) {
236            throw new IllegalArgumentException(Constants.VAR_RUNTIME_CHAIN + " is reserved, not writable");
237        }
238        return resolve(vars.put(key, value));
239    }
240
241    @Override
242    public Object remove(Object key) {
243        if (Constants.VAR_RUNTIME_CHAIN.equals(key)) {
244            throw new IllegalArgumentException(Constants.VAR_RUNTIME_CHAIN + " is reserved, not writable");
245        }
246        return resolve(vars.remove(key));
247    }
248
249    @SuppressWarnings("unchecked")
250    public Map<String, Object> getChainParameters() {
251        return (Map<String, Object>) vars.getOrDefault(Constants.VAR_RUNTIME_CHAIN, new HashMap<>());
252    }
253
254    @SuppressWarnings("unchecked")
255    public Map<String, Object> putChainParameters(Map<String, ?> parameters) {
256        return (Map<String, Object>) vars.put(Constants.VAR_RUNTIME_CHAIN, parameters);
257    }
258
259    @SuppressWarnings("unchecked")
260    public Map<String, Object> removeChainParameters() {
261        return (Map<String, Object>) vars.remove(Constants.VAR_RUNTIME_CHAIN);
262    }
263
264    /**
265     * Gets the parameter associated with the given key from the chain parameters falling back on the global context.
266     *
267     * @since 10.2
268     */
269    public Object getChainParameter(String key) {
270        return Optional.ofNullable(getChainParameters().get(key)).map(this::resolve).orElse(get(key));
271    }
272
273    /**
274     * Calls the given {@code callable} after having merged the given {@code parameters} with the chain parameters
275     * stored in the {@link Constants#VAR_RUNTIME_CHAIN} context variable and restores the initial chain parameters
276     * afterwards.
277     *
278     * @since 10.2
279     */
280    public <T> T callWithChainParameters(Callable<T> callable, Map<String, Object> parameters)
281            throws OperationException {
282        Callable<Map<String, Object>> initialize = () -> {
283            Map<String, Object> initialParameters = getChainParameters();
284            Map<String, Object> mergedParameters = new HashMap<>();
285            mergedParameters.putAll(initialParameters);
286            mergedParameters.putAll(parameters);
287            putChainParameters(mergedParameters);
288            return initialParameters;
289        };
290        Consumer<Map<String, Object>> restore = this::putChainParameters;
291        return call(callable, initialize, restore);
292
293    }
294
295    /**
296     * Calls the given {@code callable} after having put a variable with {@code key=value} in the current context and
297     * restores the initial context afterwards.
298     *
299     * @since 10.2
300     */
301    public <T> T callWithContextVar(Callable<T> callable, String key, Object value) throws OperationException {
302        Callable<Object> initialize = () -> {
303            Object initialValue = vars.get(key);
304            vars.put(key, value);
305            return initialValue;
306        };
307        Consumer<Object> restore = (Object initialValue) -> {
308            if (initialValue == null) {
309                vars.remove(key);
310            } else {
311                vars.put(key, initialValue);
312            }
313        };
314        return call(callable, initialize, restore);
315    }
316
317    /**
318     * Calls the given {@code callable} after having backed up the initial state as a result of {@code initialize} and
319     * restores the initial state afterwards by calling {@code restore}.
320     */
321    protected <T, U> T call(Callable<T> callable, Callable<U> initialize, Consumer<U> restore)
322            throws OperationException {
323        U initialState = null;
324        try {
325            initialState = initialize.call();
326            return callable.call();
327        } catch (OperationException | NuxeoException e) {
328            throw e;
329        } catch (Exception e) {
330            throw new OperationException(e);
331        } finally {
332            restore.accept(initialState);
333        }
334    }
335
336    @Override
337    public Set<Map.Entry<String, Object>> entrySet() {
338        return new AbstractSet<Map.Entry<String, Object>>() {
339
340            @Override
341            public Iterator<Entry<String, Object>> iterator() {
342                Iterator<Entry<String, Object>> iterator = vars.entrySet().iterator();
343                return new Iterator<Entry<String, Object>>() {
344
345                    @Override
346                    public boolean hasNext() {
347                        return iterator.hasNext();
348                    }
349
350                    @Override
351                    public Entry<String, Object> next() {
352                        Entry<String, Object> entry = iterator.next();
353                        return new Entry<String, Object>() {
354
355                            @Override
356                            public String getKey() {
357                                return entry.getKey();
358                            }
359
360                            @Override
361                            public Object getValue() {
362                                return resolve(entry.getValue());
363                            }
364
365                            @Override
366                            public Object setValue(Object value) {
367                                Object previous = entry.setValue(value);
368                                return resolve(previous);
369                            }
370
371                        };
372                    }
373
374                    @Override
375                    public void remove() {
376                        iterator.remove();
377                    }
378
379                };
380            }
381
382            @Override
383            public int size() {
384                return vars.size();
385            }
386        };
387    }
388
389    /**
390     * @since 5.7.3
391     */
392    public OperationCallback getCallback() {
393        return callback;
394    }
395
396    /**
397     * @since 5.7.3
398     */
399    public void setCallback(OperationCallback chainCallback) {
400        callback = chainCallback;
401    }
402
403    /**
404     * @since 5.7.3
405     * @param isolate define if keeps context variables for the subcontext
406     * @param input an input object
407     * @return a subcontext
408     */
409    public OperationContext getSubContext(boolean isolate, Object input) {
410        Map<String, Object> subVars = isolate ? new HashMap<>(getVars()) : getVars();
411        OperationContext subctx = new OperationContext(getCoreSession(), subVars);
412        subctx.setInput(input);
413        subctx.setCallback(callback);
414        return subctx;
415    }
416
417    /**
418     * @since 9.1
419     * @param isolate define if keeps context variables for the subcontext
420     * @return a subcontext
421     */
422    public OperationContext getSubContext(boolean isolate) {
423        return getSubContext(isolate, getInput());
424    }
425
426    /**
427     * Evaluate the expression against this context if needed
428     *
429     * @return the resolved value
430     * @since 9.1
431     */
432    public Object resolve(Object obj) {
433        if (!(obj instanceof Expression)) {
434            return obj;
435        }
436        return ((Expression) obj).eval(this);
437    }
438
439}