001/*
002 * (C) Copyright 2013 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     bstefanescu
016 *     vpasquier <vpasquier@nuxeo.com>
017 *     slacoin <slacoin@nuxeo.com>
018 */
019package org.nuxeo.ecm.automation;
020
021import java.security.Principal;
022import java.util.ArrayList;
023import java.util.Collection;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027import java.util.Set;
028
029import org.apache.commons.logging.Log;
030import org.apache.commons.logging.LogFactory;
031import org.nuxeo.ecm.automation.core.impl.InvokableMethod;
032import org.nuxeo.ecm.automation.core.trace.Trace;
033import org.nuxeo.ecm.core.api.CoreSession;
034import org.nuxeo.runtime.api.Framework;
035import org.nuxeo.runtime.transaction.TransactionHelper;
036
037/**
038 * An operation context. Holds context objects, a context parameters map and a list of operations to run.
039 * <p>
040 * Context objects are:
041 * <ul>
042 * <li>The Operation Chain Input - optional. It will be used as the input for the first operation in the chain. If input
043 * is null then only VOID methods in the first operation will be matched.
044 * <li>A Core Session - which is optional and should be provided by the caller. (either at creation time as a
045 * constructor argument, either using the {@link #setCoreSession(CoreSession)} method. When running the operation chain
046 * in asynchronous mode another session will be created by preserving the current session credentials.
047 * </ul>
048 * <p>
049 * Each entry in the operation list contains the ID of the operation to be run and a map of operation parameters to use
050 * when initializing the operation.
051 * <p>
052 * The context parameters map can be filled with contextual information by the caller. Each operation will be able to
053 * access the contextual data at runtime and to update it if needed.
054 *
055 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
056 */
057public class OperationContext implements Map<String, Object> {
058
059    private static final Log log = LogFactory.getLog(OperationContext.class);
060
061    /**
062     * The context variables map
063     */
064    protected Map<String, Object> vars;
065
066    /**
067     * Whether to save the session at the end of the chain execution. The default is true.
068     */
069    protected boolean commit = true;
070
071    protected final transient List<CleanupHandler> cleanupHandlers;
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, List<Object>> stacks;
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 ChainCallback chainCallback;
097
098    public OperationContext() {
099        this(null, null);
100    }
101
102    public OperationContext(CoreSession session) {
103        this(session, null);
104    }
105
106    public OperationContext(CoreSession session, Map<String, Object> vars) {
107        stacks = new HashMap<String, List<Object>>();
108        cleanupHandlers = new ArrayList<CleanupHandler>();
109        loginStack = new LoginStack(session);
110        trace = new ArrayList<String>();
111        chainCallback = new ChainCallback();
112        this.vars = vars != null ? vars : new HashMap<String, Object>();
113    }
114
115    public void setCoreSession(CoreSession session) {
116        loginStack.setSession(session);
117    }
118
119    public void setCommit(boolean commit) {
120        this.commit = commit;
121    }
122
123    public boolean isCommit() {
124        return commit;
125    }
126
127    public CoreSession getCoreSession() {
128        return loginStack.getSession();
129    }
130
131    public LoginStack getLoginStack() {
132        return loginStack;
133    }
134
135    public Principal getPrincipal() {
136        CoreSession session = loginStack.getSession();
137        return session != null ? session.getPrincipal() : null;
138    }
139
140    public void setInput(Object input) {
141        this.input = input;
142    }
143
144    public Object getInput() {
145        return input;
146    }
147
148    public Object peek(String type) {
149        List<Object> stack = stacks.get(type);
150        if (stack == null) {
151            return null;
152        }
153        return stack.isEmpty() ? null : stack.get(stack.size() - 1);
154    }
155
156    public void push(String type, Object obj) {
157        List<Object> stack = stacks.get(type);
158        if (stack == null) {
159            stack = new ArrayList<Object>();
160            stacks.put(type, stack);
161        }
162        stack.add(obj);
163    }
164
165    public Object pop(String type) {
166        List<Object> stack = stacks.get(type);
167        if (stack == null) {
168            return null;
169        }
170        return stack.isEmpty() ? null : stack.remove(stack.size() - 1);
171    }
172
173    public Object pull(String type) {
174        List<Object> stack = stacks.get(type);
175        if (stack == null) {
176            return null;
177        }
178        return stack.isEmpty() ? null : stack.remove(0);
179    }
180
181    public <T> T getAdapter(Class<T> type) {
182        if (type.isAssignableFrom(getClass())) {
183            return type.cast(this);
184        } else if (type.isAssignableFrom(CoreSession.class)) {
185            return type.cast(getCoreSession());
186        } else if (type.isAssignableFrom(Principal.class)) {
187            return type.cast(getPrincipal());
188        } else { // try nuxeo services
189            return Framework.getService(type);
190        }
191    }
192
193    /**
194     * since 5.7.3 #addTrace is no longer useful for tracing. Use chain call backs to do it.
195     */
196    @Deprecated
197    public void addTrace(String trace) {
198        this.trace.add(trace);
199    }
200
201    /**
202     * since 5.7.3 #getTrace is no longer useful for tracing. Use chain call backs to do it.
203     */
204    @Deprecated
205    public List<String> getTrace() {
206        return trace;
207    }
208
209    /**
210     * since 5.7.3 #getFormattedTrace is no longer useful for tracing. Use chain call backs to do it.
211     */
212    @Deprecated
213    public String getFormattedTrace() {
214        String crlf = System.getProperty("line.separator");
215        StringBuilder buf = new StringBuilder();
216        for (String t : trace) {
217            buf.append("> ").append(t).append(crlf);
218        }
219        return buf.toString();
220    }
221
222    public void addCleanupHandler(CleanupHandler handler) {
223        cleanupHandlers.add(handler);
224    }
225
226    public void removeCleanupHandler(CleanupHandler handler) {
227        cleanupHandlers.remove(handler);
228    }
229
230    public void dispose() throws OperationException {
231        trace.clear();
232        loginStack.clear();
233        for (CleanupHandler handler : cleanupHandlers) {
234            handler.cleanup();
235        }
236    }
237
238    /**
239     * Set the rollback mark on the current tx. This will cause the transaction to rollback. Also this is setting the
240     * session commit flag on false
241     */
242    public void setRollback() {
243        setCommit(false);
244        TransactionHelper.setTransactionRollbackOnly();
245    }
246
247    public Map<String, Object> getVars() {
248        return vars;
249    }
250
251    /** the map API */
252
253    @Override
254    public int size() {
255        return vars.size();
256    }
257
258    @Override
259    public boolean isEmpty() {
260        return vars.isEmpty();
261    }
262
263    @Override
264    public boolean containsKey(Object key) {
265        return vars.containsKey(key);
266    }
267
268    @Override
269    public boolean containsValue(Object value) {
270        return vars.containsValue(value);
271    }
272
273    @Override
274    public Object get(Object key) {
275        return vars.get(key);
276    }
277
278    @Override
279    public Object put(String key, Object value) {
280        return vars.put(key, value);
281    }
282
283    @Override
284    public Object remove(Object key) {
285        return vars.remove(key);
286    }
287
288    @Override
289    public void putAll(Map<? extends String, ? extends Object> m) {
290        vars.putAll(m);
291    }
292
293    @Override
294    public void clear() {
295        vars.clear();
296    }
297
298    @Override
299    public Set<String> keySet() {
300        return vars.keySet();
301    }
302
303    @Override
304    public Collection<Object> values() {
305        return vars.values();
306    }
307
308    @Override
309    public Set<java.util.Map.Entry<String, Object>> entrySet() {
310        return vars.entrySet();
311    }
312
313    /**
314     * ChainCallback store all automation traces for execution
315     *
316     * @since 5.7.3
317     */
318    protected static class ChainCallback implements OperationCallback {
319
320        public OperationCallback operationCallback;
321
322        protected void set(OperationCallback callback) {
323            operationCallback = callback;
324        }
325
326        @Override
327        public void onChain(OperationType chain) {
328            operationCallback.onChain(chain);
329        }
330
331        @Override
332        public void onOperation(OperationContext context, OperationType type, InvokableMethod method,
333                Map<String, Object> parms) {
334            operationCallback.onOperation(context, type, method, parms);
335        }
336
337        @Override
338        public void onError(OperationException error) {
339            operationCallback.onError(error);
340        }
341
342        @Override
343        public void onOutput(Object output) {
344            operationCallback.onOutput(output);
345        }
346
347        @Override
348        public Trace getTrace() {
349            return operationCallback.getTrace();
350        }
351
352        @Override
353        public String getFormattedText() {
354            throw new UnsupportedOperationException("#getFormattedText is not available for: " + this);
355        }
356
357    }
358
359    /**
360     * @since 5.7.3
361     */
362    public OperationCallback getChainCallback() {
363        return chainCallback.operationCallback;
364    }
365
366    /**
367     * @since 5.7.3
368     */
369    public void addChainCallback(OperationCallback chainCallback) {
370        this.chainCallback.set(chainCallback);
371    }
372
373    /**
374     * @since 5.7.3
375     * @param isolate define if keeps context variables for the subcontext
376     * @return a subcontext
377     */
378    public OperationContext getSubContext(Boolean isolate, Object input) {
379        Map<String, Object> vars = isolate ? new HashMap<String, Object>(getVars()) : getVars();
380        OperationContext subctx = new OperationContext(getCoreSession(), vars);
381        subctx.setInput(input);
382        subctx.addChainCallback(getChainCallback());
383        return subctx;
384    }
385}