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-2.1.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.core.impl; 020 021import java.util.ArrayList; 022import java.util.Collection; 023import java.util.Collections; 024import java.util.HashMap; 025import java.util.HashSet; 026import java.util.List; 027import java.util.Map; 028 029import com.google.common.collect.Iterables; 030import org.apache.commons.logging.Log; 031import org.apache.commons.logging.LogFactory; 032import org.codehaus.jackson.JsonNode; 033import org.codehaus.jackson.map.ObjectMapper; 034import org.nuxeo.ecm.automation.AutomationAdmin; 035import org.nuxeo.ecm.automation.AutomationFilter; 036import org.nuxeo.ecm.automation.AutomationService; 037import org.nuxeo.ecm.automation.ChainException; 038import org.nuxeo.ecm.automation.CompiledChain; 039import org.nuxeo.ecm.automation.OperationCallback; 040import org.nuxeo.ecm.automation.OperationChain; 041import org.nuxeo.ecm.automation.OperationCompoundExceptionBuilder; 042import org.nuxeo.ecm.automation.OperationContext; 043import org.nuxeo.ecm.automation.OperationDocumentation; 044import org.nuxeo.ecm.automation.OperationException; 045import org.nuxeo.ecm.automation.OperationNotFoundException; 046import org.nuxeo.ecm.automation.OperationParameters; 047import org.nuxeo.ecm.automation.OperationType; 048import org.nuxeo.ecm.automation.TraceException; 049import org.nuxeo.ecm.automation.TypeAdapter; 050import org.nuxeo.ecm.automation.core.Constants; 051import org.nuxeo.ecm.automation.core.exception.CatchChainException; 052import org.nuxeo.ecm.automation.core.exception.ChainExceptionRegistry; 053import org.nuxeo.ecm.automation.core.trace.TracerFactory; 054import org.nuxeo.ecm.platform.forms.layout.api.WidgetDefinition; 055import org.nuxeo.runtime.api.Framework; 056import org.nuxeo.runtime.transaction.TransactionHelper; 057 058/** 059 * The operation registry is thread safe and optimized for modifications at startup and lookups at runtime. 060 * 061 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a> 062 */ 063public class OperationServiceImpl implements AutomationService, AutomationAdmin { 064 065 private static final Log log = LogFactory.getLog(OperationServiceImpl.class); 066 067 protected final OperationTypeRegistry operations; 068 069 protected final ChainExceptionRegistry chainExceptionRegistry; 070 071 protected final AutomationFilterRegistry automationFilterRegistry; 072 073 protected Map<CacheKey, CompiledChainImpl> compiledChains = new HashMap<CacheKey, CompiledChainImpl>(); 074 075 /** 076 * Adapter registry. 077 */ 078 protected AdapterKeyedRegistry adapters; 079 080 public OperationServiceImpl() { 081 operations = new OperationTypeRegistry(); 082 adapters = new AdapterKeyedRegistry(); 083 chainExceptionRegistry = new ChainExceptionRegistry(); 084 automationFilterRegistry = new AutomationFilterRegistry(); 085 } 086 087 @Override 088 public Object run(OperationContext ctx, String operationId) throws OperationException { 089 OperationType operationType = getOperation(operationId); 090 if (operationType instanceof ChainTypeImpl) { 091 return run(ctx, operationType, ((ChainTypeImpl) operationType).getChainParameters()); 092 } else { 093 return run(ctx, operationType, null); 094 } 095 } 096 097 @Override 098 public Object run(OperationContext ctx, OperationChain chain) throws OperationException { 099 Map<String, Object> chainParameters = Collections.<String, Object> emptyMap(); 100 if (!chain.getChainParameters().isEmpty()) { 101 chainParameters = chain.getChainParameters(); 102 } 103 ChainTypeImpl chainType = new ChainTypeImpl(this, chain); 104 return run(ctx, chainType, chainParameters); 105 } 106 107 /** 108 * TODO avoid creating a temporary chain and then compile it. try to find a way to execute the single operation 109 * without compiling it. (for optimization) 110 */ 111 @Override 112 public Object run(OperationContext ctx, String operationId, Map<String, Object> runtimeParameters) 113 throws OperationException { 114 OperationType type = getOperation(operationId); 115 return run(ctx, type, runtimeParameters); 116 } 117 118 /** 119 * {@inheritDoc} 120 */ 121 @Override 122 @SuppressWarnings("unchecked") 123 public Object runInNewTx(OperationContext ctx, String chainId, Map chainParameters, Integer timeout, 124 boolean rollbackGlobalOnError) throws OperationException { 125 Object result = null; 126 // if the current transaction was already marked for rollback, 127 // do nothing 128 if (TransactionHelper.isTransactionMarkedRollback()) { 129 return null; 130 } 131 // commit the current transaction 132 TransactionHelper.commitOrRollbackTransaction(); 133 134 int to = timeout == null ? 0 : timeout; 135 136 TransactionHelper.startTransaction(to); 137 boolean ok = false; 138 139 try { 140 result = run(ctx, chainId, chainParameters); 141 ok = true; 142 } catch (OperationException e) { 143 if (rollbackGlobalOnError) { 144 throw e; 145 } else { 146 // just log, no rethrow 147 log.error("Error while executing operation " + chainId, e); 148 } 149 } finally { 150 if (!ok) { 151 // will be logged by Automation framework 152 TransactionHelper.setTransactionRollbackOnly(); 153 } 154 TransactionHelper.commitOrRollbackTransaction(); 155 // caller expects a transaction to be started 156 TransactionHelper.startTransaction(); 157 } 158 return result; 159 } 160 161 /** 162 * @since 5.7.2 163 * @param ctx the operation context. 164 * @param operationType a chain or an operation. 165 * @param params The chain parameters. 166 */ 167 public Object run(OperationContext ctx, OperationType operationType, Map<String, Object> params) 168 throws OperationException { 169 Boolean mainChain = true; 170 CompiledChainImpl chain; 171 if (params == null) { 172 params = new HashMap<>(); 173 } 174 ctx.put(Constants.VAR_RUNTIME_CHAIN, params); 175 // Put Chain parameters into the context - even for cached chains 176 if (!params.isEmpty()) { 177 ctx.put(Constants.VAR_RUNTIME_CHAIN, params); 178 } 179 OperationCallback tracer; 180 TracerFactory tracerFactory = Framework.getLocalService(TracerFactory.class); 181 if (ctx.getChainCallback() == null) { 182 tracer = tracerFactory.newTracer(operationType.getId()); 183 ctx.addChainCallback(tracer); 184 } else { 185 // Not logging at output if success for a child chain 186 mainChain = false; 187 tracer = ctx.getChainCallback(); 188 } 189 try { 190 Object input = ctx.getInput(); 191 Class<?> inputType = input == null ? Void.TYPE : input.getClass(); 192 tracer.onChain(operationType); 193 if (ChainTypeImpl.class.isAssignableFrom(operationType.getClass())) { 194 ctx.put(Constants.VAR_IS_CHAIN, true); 195 CacheKey cacheKey = new CacheKey(operationType.getId(), inputType.getName()); 196 chain = compiledChains.get(cacheKey); 197 if (chain == null) { 198 chain = (CompiledChainImpl) operationType.newInstance(ctx, params); 199 // Registered Chains are the only ones that can be cached 200 // Runtime ones can update their operations, model... 201 if (hasOperation(operationType.getId())) { 202 compiledChains.put(cacheKey, chain); 203 } 204 } 205 } else { 206 chain = CompiledChainImpl.buildChain(inputType, toParams(operationType.getId())); 207 } 208 Object ret = chain.invoke(ctx); 209 tracer.onOutput(ret); 210 if (ctx.getCoreSession() != null && ctx.isCommit()) { 211 // auto save session if any. 212 ctx.getCoreSession().save(); 213 } 214 // Log at the end of the main chain execution. 215 if (mainChain && tracer.getTrace() != null && tracerFactory.getRecordingState()) { 216 log.info(tracer.getFormattedText()); 217 } 218 return ret; 219 } catch (OperationException oe) { 220 // Record trace 221 tracer.onError(oe); 222 // Handle exception chain and rollback 223 String operationTypeId = operationType.getId(); 224 if (hasChainException(operationTypeId)) { 225 // Rollback is handled by chain exception 226 return run(ctx, getChainExceptionToRun(ctx, operationTypeId, oe)); 227 } else if (oe.isRollback()) { 228 ctx.setRollback(); 229 } 230 // Handle exception 231 if (mainChain) { 232 throw new TraceException(tracer, oe); 233 } else { 234 throw new TraceException(oe); 235 } 236 } finally { 237 ctx.dispose(); 238 } 239 } 240 241 /** 242 * @since 5.7.3 Fetch the right chain id to run when catching exception for given chain failure. 243 */ 244 protected String getChainExceptionToRun(OperationContext ctx, String operationTypeId, OperationException oe) 245 throws OperationException { 246 // Inject exception name into the context 247 //since 6.0-HF05 should use exceptionName and exceptionObject on the context instead of Exception 248 ctx.put("Exception", oe.getClass().getSimpleName()); 249 ctx.put("exceptionName", oe.getClass().getSimpleName()); 250 ctx.put("exceptionObject", oe); 251 252 ChainException chainException = getChainException(operationTypeId); 253 CatchChainException catchChainException = new CatchChainException(); 254 for (CatchChainException catchChainExceptionItem : chainException.getCatchChainExceptions()) { 255 // Check first a possible filter value 256 if (catchChainExceptionItem.hasFilter()) { 257 AutomationFilter filter = getAutomationFilter(catchChainExceptionItem.getFilterId()); 258 try { 259 String filterValue = (String) filter.getValue().eval(ctx); 260 // Check if priority for this chain exception is higher 261 if (Boolean.parseBoolean(filterValue)) { 262 catchChainException = getCatchChainExceptionByPriority(catchChainException, 263 catchChainExceptionItem); 264 } 265 } catch (RuntimeException e) { // TODO more specific exceptions? 266 throw new OperationException("Cannot evaluate Automation Filter " + filter.getId() 267 + " mvel expression.", e); 268 } 269 } else { 270 // Check if priority for this chain exception is higher 271 catchChainException = getCatchChainExceptionByPriority(catchChainException, catchChainExceptionItem); 272 } 273 } 274 String chainId = catchChainException.getChainId(); 275 if (chainId.isEmpty()) { 276 throw new OperationException( 277 "No chain exception has been selected to be run. You should verify Automation filters applied."); 278 } 279 if (catchChainException.getRollBack()) { 280 ctx.setRollback(); 281 } 282 return catchChainException.getChainId(); 283 } 284 285 /** 286 * @since 5.7.3 287 */ 288 protected CatchChainException getCatchChainExceptionByPriority(CatchChainException catchChainException, 289 CatchChainException catchChainExceptionItem) { 290 return catchChainException.getPriority() <= catchChainExceptionItem.getPriority() ? catchChainExceptionItem 291 : catchChainException; 292 } 293 294 public static OperationParameters[] toParams(String... ids) { 295 OperationParameters[] operationParameters = new OperationParameters[ids.length]; 296 for (int i = 0; i < ids.length; ++i) { 297 operationParameters[i] = new OperationParameters(ids[i]); 298 } 299 return operationParameters; 300 } 301 302 @Override 303 public synchronized void putOperationChain(OperationChain chain) throws OperationException { 304 putOperationChain(chain, false); 305 } 306 307 @Override 308 public synchronized void putOperationChain(OperationChain chain, boolean replace) throws OperationException { 309 OperationType docChainType = new ChainTypeImpl(this, chain); 310 this.putOperation(docChainType, replace); 311 } 312 313 @Override 314 public synchronized void removeOperationChain(String id) { 315 OperationChain chain = new OperationChain(id); 316 OperationType docChainType = new ChainTypeImpl(this, chain); 317 operations.removeContribution(docChainType); 318 } 319 320 @Override 321 public OperationChain getOperationChain(String id) throws OperationNotFoundException { 322 ChainTypeImpl chain = (ChainTypeImpl) getOperation(id); 323 return chain.getChain(); 324 } 325 326 @Override 327 public List<OperationChain> getOperationChains() { 328 List<ChainTypeImpl> chainsType = new ArrayList<ChainTypeImpl>(); 329 List<OperationChain> chains = new ArrayList<OperationChain>(); 330 for (OperationType operationType : operations.lookup().values()) { 331 if (operationType instanceof ChainTypeImpl) { 332 chainsType.add((ChainTypeImpl) operationType); 333 } 334 } 335 for (ChainTypeImpl chainType : chainsType) { 336 chains.add(chainType.getChain()); 337 } 338 return chains; 339 } 340 341 @Override 342 public synchronized void flushCompiledChains() { 343 compiledChains.clear(); 344 } 345 346 @Override 347 public void putOperation(Class<?> type) throws OperationException { 348 OperationTypeImpl op = new OperationTypeImpl(this, type); 349 putOperation(op, false); 350 } 351 352 @Override 353 public void putOperation(Class<?> type, boolean replace) throws OperationException { 354 putOperation(type, replace, null); 355 } 356 357 @Override 358 public void putOperation(Class<?> type, boolean replace, String contributingComponent) throws OperationException { 359 OperationTypeImpl op = new OperationTypeImpl(this, type, contributingComponent); 360 putOperation(op, replace); 361 } 362 363 @Override 364 public void putOperation(Class<?> type, boolean replace, String contributingComponent, 365 List<WidgetDefinition> widgetDefinitionList) throws OperationException { 366 OperationTypeImpl op = new OperationTypeImpl(this, type, contributingComponent, widgetDefinitionList); 367 putOperation(op, replace); 368 } 369 370 @Override 371 public synchronized void putOperation(OperationType op, boolean replace) throws OperationException { 372 operations.addContribution(op, replace); 373 } 374 375 @Override 376 public synchronized void removeOperation(Class<?> key) { 377 OperationType type = operations.getOperationType(key); 378 if (type == null) { 379 log.warn("Cannot remove operation, no such operation " + key); 380 return; 381 } 382 removeOperation(type); 383 } 384 385 @Override 386 public synchronized void removeOperation(OperationType type) { 387 operations.removeContribution(type); 388 } 389 390 @Override 391 public OperationType[] getOperations() { 392 HashSet<OperationType> values = new HashSet<>(operations.lookup().values()); 393 return values.toArray(new OperationType[values.size()]); 394 } 395 396 @Override 397 public OperationType getOperation(String id) throws OperationNotFoundException { 398 OperationType op = operations.lookup().get(id); 399 if (op == null) { 400 throw new OperationNotFoundException("No operation was bound on ID: " + id); 401 } 402 return op; 403 } 404 405 /** 406 * @since 5.7.2 407 * @param id operation ID. 408 * @return true if operation registry contains the given operation. 409 */ 410 @Override 411 public boolean hasOperation(String id) { 412 OperationType op = operations.lookup().get(id); 413 if (op == null) { 414 return false; 415 } 416 return true; 417 } 418 419 @Override 420 public CompiledChain compileChain(Class<?> inputType, OperationChain chain) throws OperationException { 421 List<OperationParameters> ops = chain.getOperations(); 422 return compileChain(inputType, ops.toArray(new OperationParameters[ops.size()])); 423 } 424 425 @Override 426 public CompiledChain compileChain(Class<?> inputType, OperationParameters... operations) throws OperationException { 427 return CompiledChainImpl.buildChain(this, inputType == null ? Void.TYPE : inputType, operations); 428 } 429 430 @Override 431 public void putTypeAdapter(Class<?> accept, Class<?> produce, TypeAdapter adapter) { 432 adapters.put(new TypeAdapterKey(accept, produce), adapter); 433 } 434 435 @Override 436 public void removeTypeAdapter(Class<?> accept, Class<?> produce) { 437 adapters.remove(new TypeAdapterKey(accept, produce)); 438 } 439 440 @Override 441 public TypeAdapter getTypeAdapter(Class<?> accept, Class<?> produce) { 442 return adapters.get(new TypeAdapterKey(accept, produce)); 443 } 444 445 @Override 446 public boolean isTypeAdaptable(Class<?> typeToAdapt, Class<?> targetType) { 447 return getTypeAdapter(typeToAdapt, targetType) != null; 448 } 449 450 @Override 451 @SuppressWarnings("unchecked") 452 public <T> T getAdaptedValue(OperationContext ctx, Object toAdapt, Class<?> targetType) throws OperationException { 453 if (toAdapt == null) { 454 return null; 455 } 456 // handle primitive types 457 Class<?> toAdaptClass = toAdapt.getClass(); 458 if (targetType.isPrimitive()) { 459 targetType = getTypeForPrimitive(targetType); 460 if (targetType.isAssignableFrom(toAdaptClass)) { 461 return (T) toAdapt; 462 } 463 } 464 if (targetType.isArray() && toAdapt instanceof List) { 465 return (T) Iterables.toArray((Iterable) toAdapt, targetType.getComponentType()); 466 } 467 TypeAdapter adapter = getTypeAdapter(toAdaptClass, targetType); 468 if (adapter == null) { 469 if (toAdapt instanceof JsonNode) { 470 // fall-back to generic jackson adapter 471 ObjectMapper mapper = new ObjectMapper(); 472 return (T) mapper.convertValue(toAdapt, targetType); 473 } 474 throw new OperationException("No type adapter found for input: " + toAdapt.getClass() + " and output " 475 + targetType); 476 } 477 return (T) adapter.getAdaptedValue(ctx, toAdapt); 478 } 479 480 @Override 481 public List<OperationDocumentation> getDocumentation() throws OperationException { 482 List<OperationDocumentation> result = new ArrayList<OperationDocumentation>(); 483 HashSet<OperationType> ops = new HashSet<>(operations.lookup().values()); 484 OperationCompoundExceptionBuilder errorBuilder = new OperationCompoundExceptionBuilder(); 485 for (OperationType ot : ops.toArray(new OperationType[ops.size()])) { 486 try { 487 result.add(ot.getDocumentation()); 488 } catch (OperationNotFoundException e) { 489 errorBuilder.add(e); 490 } 491 } 492 errorBuilder.throwOnError(); 493 Collections.sort(result); 494 return result; 495 } 496 497 public static Class<?> getTypeForPrimitive(Class<?> primitiveType) { 498 if (primitiveType == Boolean.TYPE) { 499 return Boolean.class; 500 } else if (primitiveType == Integer.TYPE) { 501 return Integer.class; 502 } else if (primitiveType == Long.TYPE) { 503 return Long.class; 504 } else if (primitiveType == Float.TYPE) { 505 return Float.class; 506 } else if (primitiveType == Double.TYPE) { 507 return Double.class; 508 } else if (primitiveType == Character.TYPE) { 509 return Character.class; 510 } else if (primitiveType == Byte.TYPE) { 511 return Byte.class; 512 } else if (primitiveType == Short.TYPE) { 513 return Short.class; 514 } 515 return primitiveType; 516 } 517 518 /** 519 * @since 5.7.3 520 */ 521 @Override 522 public void putChainException(ChainException exceptionChain) { 523 chainExceptionRegistry.addContribution(exceptionChain); 524 } 525 526 /** 527 * @since 5.7.3 528 */ 529 @Override 530 public void removeExceptionChain(ChainException exceptionChain) { 531 chainExceptionRegistry.removeContribution(exceptionChain); 532 } 533 534 /** 535 * @since 5.7.3 536 */ 537 @Override 538 public ChainException[] getChainExceptions() { 539 Collection<ChainException> chainExceptions = chainExceptionRegistry.lookup().values(); 540 return chainExceptions.toArray(new ChainException[chainExceptions.size()]); 541 } 542 543 /** 544 * @since 5.7.3 545 */ 546 @Override 547 public ChainException getChainException(String onChainId) { 548 return chainExceptionRegistry.getChainException(onChainId); 549 } 550 551 /** 552 * @since 5.7.3 553 */ 554 @Override 555 public boolean hasChainException(String onChainId) { 556 return chainExceptionRegistry.getChainException(onChainId) != null; 557 } 558 559 /** 560 * @since 5.7.3 561 */ 562 @Override 563 public void putAutomationFilter(AutomationFilter automationFilter) { 564 automationFilterRegistry.addContribution(automationFilter); 565 } 566 567 /** 568 * @since 5.7.3 569 */ 570 @Override 571 public void removeAutomationFilter(AutomationFilter automationFilter) { 572 automationFilterRegistry.removeContribution(automationFilter); 573 } 574 575 /** 576 * @since 5.7.3 577 */ 578 @Override 579 public AutomationFilter getAutomationFilter(String id) { 580 return automationFilterRegistry.getAutomationFilter(id); 581 } 582 583 /** 584 * @since 5.7.3 585 */ 586 @Override 587 public AutomationFilter[] getAutomationFilters() { 588 Collection<AutomationFilter> automationFilters = automationFilterRegistry.lookup().values(); 589 return automationFilters.toArray(new AutomationFilter[automationFilters.size()]); 590 } 591 592 /** 593 * @since 5.8 - Composite key to handle several operations with same id and different input types. 594 */ 595 protected static class CacheKey { 596 597 String operationId; 598 599 String inputType; 600 601 public CacheKey(String operationId, String inputType) { 602 this.operationId = operationId; 603 this.inputType = inputType; 604 } 605 606 @Override 607 public boolean equals(Object o) { 608 if (this == o) { 609 return true; 610 } 611 if (o == null || getClass() != o.getClass()) { 612 return false; 613 } 614 615 CacheKey cacheKey = (CacheKey) o; 616 617 if (inputType != null ? !inputType.equals(cacheKey.inputType) : cacheKey.inputType != null) { 618 return false; 619 } 620 if (operationId != null ? !operationId.equals(cacheKey.operationId) : cacheKey.operationId != null) { 621 return false; 622 } 623 624 return true; 625 } 626 627 @Override 628 public int hashCode() { 629 int result = operationId != null ? operationId.hashCode() : 0; 630 result = 31 * result + (inputType != null ? inputType.hashCode() : 0); 631 return result; 632 } 633 } 634}