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 <[email protected]> 019 * slacoin <[email protected]> 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:[email protected]">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 * @param obj 430 * @return the resolved value 431 * @since 9.1 432 */ 433 public Object resolve(Object obj) { 434 if (!(obj instanceof Expression)) { 435 return obj; 436 } 437 return ((Expression) obj).eval(this); 438 } 439 440}