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 CacheKey cacheKey = new CacheKey(operationType.getId(), inputType.getName()); 195 chain = compiledChains.get(cacheKey); 196 if (chain == null) { 197 chain = (CompiledChainImpl) operationType.newInstance(ctx, params); 198 // Registered Chains are the only ones that can be cached 199 // Runtime ones can update their operations, model... 200 if (hasOperation(operationType.getId())) { 201 compiledChains.put(cacheKey, chain); 202 } 203 } 204 } else { 205 chain = CompiledChainImpl.buildChain(inputType, toParams(operationType.getId())); 206 } 207 Object ret = chain.invoke(ctx); 208 tracer.onOutput(ret); 209 if (ctx.getCoreSession() != null && ctx.isCommit()) { 210 // auto save session if any. 211 ctx.getCoreSession().save(); 212 } 213 // Log at the end of the main chain execution. 214 if (mainChain && tracer.getTrace() != null && tracerFactory.getRecordingState()) { 215 log.info(tracer.getFormattedText()); 216 } 217 return ret; 218 } catch (OperationException oe) { 219 // Record trace 220 tracer.onError(oe); 221 // Handle exception chain and rollback 222 String operationTypeId = operationType.getId(); 223 if (hasChainException(operationTypeId)) { 224 // Rollback is handled by chain exception 225 return run(ctx, getChainExceptionToRun(ctx, operationTypeId, oe)); 226 } else if (oe.isRollback()) { 227 ctx.setRollback(); 228 } 229 // Handle exception 230 if (mainChain) { 231 throw new TraceException(tracer, oe); 232 } else { 233 throw new TraceException(oe); 234 } 235 } finally { 236 ctx.dispose(); 237 } 238 } 239 240 /** 241 * @since 5.7.3 Fetch the right chain id to run when catching exception for given chain failure. 242 */ 243 protected String getChainExceptionToRun(OperationContext ctx, String operationTypeId, OperationException oe) 244 throws OperationException { 245 // Inject exception name into the context 246 //since 6.0-HF05 should use exceptionName and exceptionObject on the context instead of Exception 247 ctx.put("Exception", oe.getClass().getSimpleName()); 248 ctx.put("exceptionName", oe.getClass().getSimpleName()); 249 ctx.put("exceptionObject", oe); 250 251 ChainException chainException = getChainException(operationTypeId); 252 CatchChainException catchChainException = new CatchChainException(); 253 for (CatchChainException catchChainExceptionItem : chainException.getCatchChainExceptions()) { 254 // Check first a possible filter value 255 if (catchChainExceptionItem.hasFilter()) { 256 AutomationFilter filter = getAutomationFilter(catchChainExceptionItem.getFilterId()); 257 try { 258 String filterValue = (String) filter.getValue().eval(ctx); 259 // Check if priority for this chain exception is higher 260 if (Boolean.parseBoolean(filterValue)) { 261 catchChainException = getCatchChainExceptionByPriority(catchChainException, 262 catchChainExceptionItem); 263 } 264 } catch (RuntimeException e) { // TODO more specific exceptions? 265 throw new OperationException("Cannot evaluate Automation Filter " + filter.getId() 266 + " mvel expression.", e); 267 } 268 } else { 269 // Check if priority for this chain exception is higher 270 catchChainException = getCatchChainExceptionByPriority(catchChainException, catchChainExceptionItem); 271 } 272 } 273 String chainId = catchChainException.getChainId(); 274 if (chainId.isEmpty()) { 275 throw new OperationException( 276 "No chain exception has been selected to be run. You should verify Automation filters applied."); 277 } 278 if (catchChainException.getRollBack()) { 279 ctx.setRollback(); 280 } 281 return catchChainException.getChainId(); 282 } 283 284 /** 285 * @since 5.7.3 286 */ 287 protected CatchChainException getCatchChainExceptionByPriority(CatchChainException catchChainException, 288 CatchChainException catchChainExceptionItem) { 289 return catchChainException.getPriority() <= catchChainExceptionItem.getPriority() ? catchChainExceptionItem 290 : catchChainException; 291 } 292 293 public static OperationParameters[] toParams(String... ids) { 294 OperationParameters[] operationParameters = new OperationParameters[ids.length]; 295 for (int i = 0; i < ids.length; ++i) { 296 operationParameters[i] = new OperationParameters(ids[i]); 297 } 298 return operationParameters; 299 } 300 301 @Override 302 public synchronized void putOperationChain(OperationChain chain) throws OperationException { 303 putOperationChain(chain, false); 304 } 305 306 @Override 307 public synchronized void putOperationChain(OperationChain chain, boolean replace) throws OperationException { 308 OperationType docChainType = new ChainTypeImpl(this, chain); 309 this.putOperation(docChainType, replace); 310 } 311 312 @Override 313 public synchronized void removeOperationChain(String id) { 314 OperationChain chain = new OperationChain(id); 315 OperationType docChainType = new ChainTypeImpl(this, chain); 316 operations.removeContribution(docChainType); 317 } 318 319 @Override 320 public OperationChain getOperationChain(String id) throws OperationNotFoundException { 321 ChainTypeImpl chain = (ChainTypeImpl) getOperation(id); 322 return chain.getChain(); 323 } 324 325 @Override 326 public List<OperationChain> getOperationChains() { 327 List<ChainTypeImpl> chainsType = new ArrayList<ChainTypeImpl>(); 328 List<OperationChain> chains = new ArrayList<OperationChain>(); 329 for (OperationType operationType : operations.lookup().values()) { 330 if (operationType instanceof ChainTypeImpl) { 331 chainsType.add((ChainTypeImpl) operationType); 332 } 333 } 334 for (ChainTypeImpl chainType : chainsType) { 335 chains.add(chainType.getChain()); 336 } 337 return chains; 338 } 339 340 @Override 341 public synchronized void flushCompiledChains() { 342 compiledChains.clear(); 343 } 344 345 @Override 346 public void putOperation(Class<?> type) throws OperationException { 347 OperationTypeImpl op = new OperationTypeImpl(this, type); 348 putOperation(op, false); 349 } 350 351 @Override 352 public void putOperation(Class<?> type, boolean replace) throws OperationException { 353 putOperation(type, replace, null); 354 } 355 356 @Override 357 public void putOperation(Class<?> type, boolean replace, String contributingComponent) throws OperationException { 358 OperationTypeImpl op = new OperationTypeImpl(this, type, contributingComponent); 359 putOperation(op, replace); 360 } 361 362 @Override 363 public void putOperation(Class<?> type, boolean replace, String contributingComponent, 364 List<WidgetDefinition> widgetDefinitionList) throws OperationException { 365 OperationTypeImpl op = new OperationTypeImpl(this, type, contributingComponent, widgetDefinitionList); 366 putOperation(op, replace); 367 } 368 369 @Override 370 public synchronized void putOperation(OperationType op, boolean replace) throws OperationException { 371 operations.addContribution(op, replace); 372 } 373 374 @Override 375 public synchronized void removeOperation(Class<?> key) { 376 OperationType type = operations.getOperationType(key); 377 if (type == null) { 378 log.warn("Cannot remove operation, no such operation " + key); 379 return; 380 } 381 removeOperation(type); 382 } 383 384 @Override 385 public synchronized void removeOperation(OperationType type) { 386 operations.removeContribution(type); 387 } 388 389 @Override 390 public OperationType[] getOperations() { 391 HashSet<OperationType> values = new HashSet<>(operations.lookup().values()); 392 return values.toArray(new OperationType[values.size()]); 393 } 394 395 @Override 396 public OperationType getOperation(String id) throws OperationNotFoundException { 397 OperationType op = operations.lookup().get(id); 398 if (op == null) { 399 throw new OperationNotFoundException("No operation was bound on ID: " + id); 400 } 401 return op; 402 } 403 404 /** 405 * @since 5.7.2 406 * @param id operation ID. 407 * @return true if operation registry contains the given operation. 408 */ 409 @Override 410 public boolean hasOperation(String id) { 411 OperationType op = operations.lookup().get(id); 412 if (op == null) { 413 return false; 414 } 415 return true; 416 } 417 418 @Override 419 public CompiledChain compileChain(Class<?> inputType, OperationChain chain) throws OperationException { 420 List<OperationParameters> ops = chain.getOperations(); 421 return compileChain(inputType, ops.toArray(new OperationParameters[ops.size()])); 422 } 423 424 @Override 425 public CompiledChain compileChain(Class<?> inputType, OperationParameters... operations) throws OperationException { 426 return CompiledChainImpl.buildChain(this, inputType == null ? Void.TYPE : inputType, operations); 427 } 428 429 @Override 430 public void putTypeAdapter(Class<?> accept, Class<?> produce, TypeAdapter adapter) { 431 adapters.put(new TypeAdapterKey(accept, produce), adapter); 432 } 433 434 @Override 435 public void removeTypeAdapter(Class<?> accept, Class<?> produce) { 436 adapters.remove(new TypeAdapterKey(accept, produce)); 437 } 438 439 @Override 440 public TypeAdapter getTypeAdapter(Class<?> accept, Class<?> produce) { 441 return adapters.get(new TypeAdapterKey(accept, produce)); 442 } 443 444 @Override 445 public boolean isTypeAdaptable(Class<?> typeToAdapt, Class<?> targetType) { 446 return getTypeAdapter(typeToAdapt, targetType) != null; 447 } 448 449 @Override 450 @SuppressWarnings("unchecked") 451 public <T> T getAdaptedValue(OperationContext ctx, Object toAdapt, Class<?> targetType) throws OperationException { 452 if (toAdapt == null) { 453 return null; 454 } 455 // handle primitive types 456 Class<?> toAdaptClass = toAdapt.getClass(); 457 if (targetType.isPrimitive()) { 458 targetType = getTypeForPrimitive(targetType); 459 if (targetType.isAssignableFrom(toAdaptClass)) { 460 return (T) toAdapt; 461 } 462 } 463 if (targetType.isArray() && toAdapt instanceof List) { 464 return (T) Iterables.toArray((Iterable) toAdapt, targetType.getComponentType()); 465 } 466 TypeAdapter adapter = getTypeAdapter(toAdaptClass, targetType); 467 if (adapter == null) { 468 if (toAdapt instanceof JsonNode) { 469 // fall-back to generic jackson adapter 470 ObjectMapper mapper = new ObjectMapper(); 471 return (T) mapper.convertValue(toAdapt, targetType); 472 } 473 throw new OperationException("No type adapter found for input: " + toAdapt.getClass() + " and output " 474 + targetType); 475 } 476 return (T) adapter.getAdaptedValue(ctx, toAdapt); 477 } 478 479 @Override 480 public List<OperationDocumentation> getDocumentation() throws OperationException { 481 List<OperationDocumentation> result = new ArrayList<OperationDocumentation>(); 482 HashSet<OperationType> ops = new HashSet<>(operations.lookup().values()); 483 OperationCompoundExceptionBuilder errorBuilder = new OperationCompoundExceptionBuilder(); 484 for (OperationType ot : ops.toArray(new OperationType[ops.size()])) { 485 try { 486 result.add(ot.getDocumentation()); 487 } catch (OperationNotFoundException e) { 488 errorBuilder.add(e); 489 } 490 } 491 errorBuilder.throwOnError(); 492 Collections.sort(result); 493 return result; 494 } 495 496 public static Class<?> getTypeForPrimitive(Class<?> primitiveType) { 497 if (primitiveType == Boolean.TYPE) { 498 return Boolean.class; 499 } else if (primitiveType == Integer.TYPE) { 500 return Integer.class; 501 } else if (primitiveType == Long.TYPE) { 502 return Long.class; 503 } else if (primitiveType == Float.TYPE) { 504 return Float.class; 505 } else if (primitiveType == Double.TYPE) { 506 return Double.class; 507 } else if (primitiveType == Character.TYPE) { 508 return Character.class; 509 } else if (primitiveType == Byte.TYPE) { 510 return Byte.class; 511 } else if (primitiveType == Short.TYPE) { 512 return Short.class; 513 } 514 return primitiveType; 515 } 516 517 /** 518 * @since 5.7.3 519 */ 520 @Override 521 public void putChainException(ChainException exceptionChain) { 522 chainExceptionRegistry.addContribution(exceptionChain); 523 } 524 525 /** 526 * @since 5.7.3 527 */ 528 @Override 529 public void removeExceptionChain(ChainException exceptionChain) { 530 chainExceptionRegistry.removeContribution(exceptionChain); 531 } 532 533 /** 534 * @since 5.7.3 535 */ 536 @Override 537 public ChainException[] getChainExceptions() { 538 Collection<ChainException> chainExceptions = chainExceptionRegistry.lookup().values(); 539 return chainExceptions.toArray(new ChainException[chainExceptions.size()]); 540 } 541 542 /** 543 * @since 5.7.3 544 */ 545 @Override 546 public ChainException getChainException(String onChainId) { 547 return chainExceptionRegistry.getChainException(onChainId); 548 } 549 550 /** 551 * @since 5.7.3 552 */ 553 @Override 554 public boolean hasChainException(String onChainId) { 555 return chainExceptionRegistry.getChainException(onChainId) != null; 556 } 557 558 /** 559 * @since 5.7.3 560 */ 561 @Override 562 public void putAutomationFilter(AutomationFilter automationFilter) { 563 automationFilterRegistry.addContribution(automationFilter); 564 } 565 566 /** 567 * @since 5.7.3 568 */ 569 @Override 570 public void removeAutomationFilter(AutomationFilter automationFilter) { 571 automationFilterRegistry.removeContribution(automationFilter); 572 } 573 574 /** 575 * @since 5.7.3 576 */ 577 @Override 578 public AutomationFilter getAutomationFilter(String id) { 579 return automationFilterRegistry.getAutomationFilter(id); 580 } 581 582 /** 583 * @since 5.7.3 584 */ 585 @Override 586 public AutomationFilter[] getAutomationFilters() { 587 Collection<AutomationFilter> automationFilters = automationFilterRegistry.lookup().values(); 588 return automationFilters.toArray(new AutomationFilter[automationFilters.size()]); 589 } 590 591 /** 592 * @since 5.8 - Composite key to handle several operations with same id and different input types. 593 */ 594 protected static class CacheKey { 595 596 String operationId; 597 598 String inputType; 599 600 public CacheKey(String operationId, String inputType) { 601 this.operationId = operationId; 602 this.inputType = inputType; 603 } 604 605 @Override 606 public boolean equals(Object o) { 607 if (this == o) { 608 return true; 609 } 610 if (o == null || getClass() != o.getClass()) { 611 return false; 612 } 613 614 CacheKey cacheKey = (CacheKey) o; 615 616 if (inputType != null ? !inputType.equals(cacheKey.inputType) : cacheKey.inputType != null) { 617 return false; 618 } 619 if (operationId != null ? !operationId.equals(cacheKey.operationId) : cacheKey.operationId != null) { 620 return false; 621 } 622 623 return true; 624 } 625 626 @Override 627 public int hashCode() { 628 int result = operationId != null ? operationId.hashCode() : 0; 629 result = 31 * result + (inputType != null ? inputType.hashCode() : 0); 630 return result; 631 } 632 } 633}