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}