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(OperationContext ctx) {
103        if (ctx.loginStack == null) {
104            this.loginStack = new LoginStack(ctx.getCoreSession());
105        } else {
106            this.loginStack = ctx.loginStack;
107        }
108        this.vars = ctx.vars;
109        this.cleanupHandlers = ctx.cleanupHandlers;
110        this.stacks = ctx.stacks;
111        this.commit = ctx.commit;
112        this.input = ctx.input;
113        this.trace = ctx.trace;
114        this.chainCallback = ctx.chainCallback;
115    }
116
117    public OperationContext(CoreSession session) {
118        this(session, null);
119    }
120
121    public OperationContext(CoreSession session, Map<String, Object> vars) {
122        stacks = new HashMap<String, List<Object>>();
123        cleanupHandlers = new ArrayList<CleanupHandler>();
124        loginStack = new LoginStack(session);
125        trace = new ArrayList<String>();
126        chainCallback = new ChainCallback();
127        this.vars = vars != null ? vars : new HashMap<String, Object>();
128    }
129
130    public void setCoreSession(CoreSession session) {
131        loginStack.setSession(session);
132    }
133
134    public void setCommit(boolean commit) {
135        this.commit = commit;
136    }
137
138    public boolean isCommit() {
139        return commit;
140    }
141
142    public CoreSession getCoreSession() {
143        return loginStack.getSession();
144    }
145
146    public LoginStack getLoginStack() {
147        return loginStack;
148    }
149
150    public Principal getPrincipal() {
151        CoreSession session = loginStack.getSession();
152        return session != null ? session.getPrincipal() : null;
153    }
154
155    public void setInput(Object input) {
156        this.input = input;
157    }
158
159    public Object getInput() {
160        return input;
161    }
162
163    public Object peek(String type) {
164        List<Object> stack = stacks.get(type);
165        if (stack == null) {
166            return null;
167        }
168        return stack.isEmpty() ? null : stack.get(stack.size() - 1);
169    }
170
171    public void push(String type, Object obj) {
172        List<Object> stack = stacks.get(type);
173        if (stack == null) {
174            stack = new ArrayList<Object>();
175            stacks.put(type, stack);
176        }
177        stack.add(obj);
178    }
179
180    public Object pop(String type) {
181        List<Object> stack = stacks.get(type);
182        if (stack == null) {
183            return null;
184        }
185        return stack.isEmpty() ? null : stack.remove(stack.size() - 1);
186    }
187
188    public Object pull(String type) {
189        List<Object> stack = stacks.get(type);
190        if (stack == null) {
191            return null;
192        }
193        return stack.isEmpty() ? null : stack.remove(0);
194    }
195
196    public <T> T getAdapter(Class<T> type) {
197        if (type.isAssignableFrom(getClass())) {
198            return type.cast(this);
199        } else if (type.isAssignableFrom(CoreSession.class)) {
200            return type.cast(getCoreSession());
201        } else if (type.isAssignableFrom(Principal.class)) {
202            return type.cast(getPrincipal());
203        } else { // try nuxeo services
204            return Framework.getService(type);
205        }
206    }
207
208    /**
209     * since 5.7.3 #addTrace is no longer useful for tracing. Use chain call backs to do it.
210     */
211    @Deprecated
212    public void addTrace(String trace) {
213        this.trace.add(trace);
214    }
215
216    /**
217     * since 5.7.3 #getTrace is no longer useful for tracing. Use chain call backs to do it.
218     */
219    @Deprecated
220    public List<String> getTrace() {
221        return trace;
222    }
223
224    /**
225     * since 5.7.3 #getFormattedTrace is no longer useful for tracing. Use chain call backs to do it.
226     */
227    @Deprecated
228    public String getFormattedTrace() {
229        String crlf = System.getProperty("line.separator");
230        StringBuilder buf = new StringBuilder();
231        for (String t : trace) {
232            buf.append("> ").append(t).append(crlf);
233        }
234        return buf.toString();
235    }
236
237    public void addCleanupHandler(CleanupHandler handler) {
238        cleanupHandlers.add(handler);
239    }
240
241    public void removeCleanupHandler(CleanupHandler handler) {
242        cleanupHandlers.remove(handler);
243    }
244
245    public void dispose() throws OperationException {
246        trace.clear();
247        loginStack.clear();
248        for (CleanupHandler handler : cleanupHandlers) {
249            handler.cleanup();
250        }
251    }
252
253    /**
254     * Set the rollback mark on the current tx. This will cause the transaction to rollback. Also this is setting the
255     * session commit flag on false
256     */
257    public void setRollback() {
258        setCommit(false);
259        TransactionHelper.setTransactionRollbackOnly();
260    }
261
262    public Map<String, Object> getVars() {
263        return vars;
264    }
265
266    /** the map API */
267
268    @Override
269    public int size() {
270        return vars.size();
271    }
272
273    @Override
274    public boolean isEmpty() {
275        return vars.isEmpty();
276    }
277
278    @Override
279    public boolean containsKey(Object key) {
280        return vars.containsKey(key);
281    }
282
283    @Override
284    public boolean containsValue(Object value) {
285        return vars.containsValue(value);
286    }
287
288    @Override
289    public Object get(Object key) {
290        return vars.get(key);
291    }
292
293    @Override
294    public Object put(String key, Object value) {
295        return vars.put(key, value);
296    }
297
298    @Override
299    public Object remove(Object key) {
300        return vars.remove(key);
301    }
302
303    @Override
304    public void putAll(Map<? extends String, ? extends Object> m) {
305        vars.putAll(m);
306    }
307
308    @Override
309    public void clear() {
310        vars.clear();
311    }
312
313    @Override
314    public Set<String> keySet() {
315        return vars.keySet();
316    }
317
318    @Override
319    public Collection<Object> values() {
320        return vars.values();
321    }
322
323    @Override
324    public Set<java.util.Map.Entry<String, Object>> entrySet() {
325        return vars.entrySet();
326    }
327
328    /**
329     * ChainCallback store all automation traces for execution
330     *
331     * @since 5.7.3
332     */
333    protected static class ChainCallback implements OperationCallback {
334
335        public OperationCallback operationCallback;
336
337        protected void set(OperationCallback callback) {
338            operationCallback = callback;
339        }
340
341        @Override
342        public void onChain(OperationType chain) {
343            operationCallback.onChain(chain);
344        }
345
346        @Override
347        public void onOperation(OperationContext context, OperationType type, InvokableMethod method,
348                Map<String, Object> parms) {
349            operationCallback.onOperation(context, type, method, parms);
350        }
351
352        @Override
353        public void onError(OperationException error) {
354            operationCallback.onError(error);
355        }
356
357        @Override
358        public void onOutput(Object output) {
359            operationCallback.onOutput(output);
360        }
361
362        @Override
363        public Trace getTrace() {
364            return operationCallback.getTrace();
365        }
366
367        @Override
368        public String getFormattedText() {
369            throw new UnsupportedOperationException("#getFormattedText is not available for: " + this);
370        }
371
372    }
373
374    /**
375     * @since 5.7.3
376     */
377    public OperationCallback getChainCallback() {
378        return chainCallback.operationCallback;
379    }
380
381    /**
382     * @since 5.7.3
383     */
384    public void addChainCallback(OperationCallback chainCallback) {
385        this.chainCallback.set(chainCallback);
386    }
387
388    /**
389     * @since 5.7.3
390     * @param isolate define if keeps context variables for the subcontext
391     * @return a subcontext
392     */
393    public OperationContext getSubContext(Boolean isolate, Object input) {
394        Map<String, Object> vars = isolate ? new HashMap<String, Object>(getVars()) : getVars();
395        OperationContext subctx = new OperationContext(getCoreSession(), vars);
396        subctx.setInput(input);
397        subctx.addChainCallback(getChainCallback());
398        return subctx;
399    }
400}