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