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