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 <[email protected]> 019 * slacoin <[email protected]> 020 */ 021package org.nuxeo.ecm.automation.core.impl; 022 023import java.util.ArrayList; 024import java.util.Arrays; 025import java.util.Collection; 026import java.util.Collections; 027import java.util.HashMap; 028import java.util.HashSet; 029import java.util.List; 030import java.util.Map; 031 032import org.apache.commons.logging.Log; 033import org.apache.commons.logging.LogFactory; 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.OperationChain; 040import org.nuxeo.ecm.automation.OperationContext; 041import org.nuxeo.ecm.automation.OperationDocumentation; 042import org.nuxeo.ecm.automation.OperationException; 043import org.nuxeo.ecm.automation.OperationNotFoundException; 044import org.nuxeo.ecm.automation.OperationParameters; 045import org.nuxeo.ecm.automation.OperationType; 046import org.nuxeo.ecm.automation.TypeAdapter; 047import org.nuxeo.ecm.automation.core.exception.CatchChainException; 048import org.nuxeo.ecm.automation.core.exception.ChainExceptionRegistry; 049import org.nuxeo.ecm.platform.forms.layout.api.WidgetDefinition; 050import org.nuxeo.runtime.api.Framework; 051import org.nuxeo.runtime.services.config.ConfigurationService; 052import org.nuxeo.runtime.transaction.TransactionHelper; 053 054import com.fasterxml.jackson.databind.JsonNode; 055import com.fasterxml.jackson.databind.ObjectMapper; 056import com.google.common.collect.Iterables; 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:[email protected]">Bogdan Stefanescu</a> 062 */ 063public class OperationServiceImpl implements AutomationService, AutomationAdmin { 064 065 private static final Log log = LogFactory.getLog(OperationServiceImpl.class); 066 067 public static final String EXPORT_ALIASES_CONFIGURATION_PARAM = "nuxeo.automation.export.aliases"; 068 069 protected final OperationTypeRegistry operations; 070 071 protected final ChainExceptionRegistry chainExceptionRegistry; 072 073 protected final AutomationFilterRegistry automationFilterRegistry; 074 075 protected final OperationChainCompiler compiler = new OperationChainCompiler(this); 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 return run(ctx, getOperationChain(operationId)); 092 } 093 094 @Override 095 @SuppressWarnings("unchecked") 096 public Object run(OperationContext ctx, String operationId, Map<String, ?> args) throws OperationException { 097 OperationType op = operations.lookup().get(operationId); 098 if (op == null) { 099 throw new IllegalArgumentException("No such operation " + operationId); 100 } 101 if (args == null) { 102 log.warn("null operation parameters given for " + operationId, new Throwable("stack trace")); 103 args = Collections.emptyMap(); 104 } 105 return ctx.callWithChainParameters(() -> run(ctx, getOperationChain(operationId)), (Map<String, Object>) args); 106 } 107 108 @Override 109 public Object run(OperationContext ctx, OperationChain chain) throws OperationException { 110 Object input = ctx.getInput(); 111 Class<?> inputType = input == null ? Void.TYPE : input.getClass(); 112 CompiledChain compiled = compileChain(inputType, chain); 113 boolean completedAbruptly = true; 114 try { 115 Object result = compiled.invoke(ctx); 116 completedAbruptly = false; 117 return result; 118 } catch (OperationException cause) { 119 completedAbruptly = false; 120 if (hasChainException(chain.getId())) { 121 return run(ctx, getChainExceptionToRun(ctx, chain.getId(), cause)); 122 } else if (cause.isRollback()) { 123 ctx.setRollback(); 124 } 125 throw cause; 126 } finally { 127 if (completedAbruptly) { 128 ctx.setRollback(); 129 } 130 } 131 } 132 133 @Override 134 public Object runInNewTx(OperationContext ctx, String chainId, Map<String, ?> chainParameters, Integer timeout, 135 boolean rollbackGlobalOnError) throws OperationException { 136 Object result = null; 137 // if the current transaction was already marked for rollback, 138 // do nothing 139 if (TransactionHelper.isTransactionMarkedRollback()) { 140 return null; 141 } 142 // commit the current transaction 143 TransactionHelper.commitOrRollbackTransaction(); 144 145 int to = timeout == null ? 0 : timeout; 146 147 TransactionHelper.startTransaction(to); 148 boolean ok = false; 149 150 try { 151 result = run(ctx, chainId, chainParameters); 152 ok = true; 153 } catch (OperationException e) { 154 if (rollbackGlobalOnError) { 155 throw e; 156 } else { 157 // just log, no rethrow 158 log.error("Error while executing operation " + chainId, e); 159 } 160 } finally { 161 if (!ok) { 162 // will be logged by Automation framework 163 TransactionHelper.setTransactionRollbackOnly(); 164 } 165 TransactionHelper.commitOrRollbackTransaction(); 166 // caller expects a transaction to be started 167 TransactionHelper.startTransaction(); 168 } 169 return result; 170 } 171 172 /** 173 * @since 5.7.3 Fetch the right chain id to run when catching exception for given chain failure. 174 */ 175 protected String getChainExceptionToRun(OperationContext ctx, String operationTypeId, OperationException oe) 176 throws OperationException { 177 // Inject exception name into the context 178 // since 6.0-HF05 should use exceptionName and exceptionObject on the context instead of Exception 179 ctx.put("Exception", oe.getClass().getSimpleName()); 180 ctx.put("exceptionName", oe.getClass().getSimpleName()); 181 ctx.put("exceptionObject", oe); 182 183 ChainException chainException = getChainException(operationTypeId); 184 CatchChainException catchChainException = new CatchChainException(); 185 for (CatchChainException catchChainExceptionItem : chainException.getCatchChainExceptions()) { 186 // Check first a possible filter value 187 if (catchChainExceptionItem.hasFilter()) { 188 AutomationFilter filter = getAutomationFilter(catchChainExceptionItem.getFilterId()); 189 try { 190 String filterValue = (String) filter.getValue().eval(ctx); 191 // Check if priority for this chain exception is higher 192 if (Boolean.parseBoolean(filterValue)) { 193 catchChainException = getCatchChainExceptionByPriority(catchChainException, 194 catchChainExceptionItem); 195 } 196 } catch (RuntimeException e) { // TODO more specific exceptions? 197 throw new OperationException( 198 "Cannot evaluate Automation Filter " + filter.getId() + " mvel expression.", e); 199 } 200 } else { 201 // Check if priority for this chain exception is higher 202 catchChainException = getCatchChainExceptionByPriority(catchChainException, catchChainExceptionItem); 203 } 204 } 205 String chainId = catchChainException.getChainId(); 206 if (chainId.isEmpty()) { 207 throw new OperationException( 208 "No chain exception has been selected to be run. You should verify Automation filters applied."); 209 } 210 if (catchChainException.getRollBack()) { 211 ctx.setRollback(); 212 } 213 return catchChainException.getChainId(); 214 } 215 216 /** 217 * @since 5.7.3 218 */ 219 protected CatchChainException getCatchChainExceptionByPriority(CatchChainException catchChainException, 220 CatchChainException catchChainExceptionItem) { 221 return catchChainException.getPriority() <= catchChainExceptionItem.getPriority() ? catchChainExceptionItem 222 : catchChainException; 223 } 224 225 public static OperationParameters[] toParams(String... ids) { 226 OperationParameters[] operationParameters = new OperationParameters[ids.length]; 227 for (int i = 0; i < ids.length; ++i) { 228 operationParameters[i] = new OperationParameters(ids[i]); 229 } 230 return operationParameters; 231 } 232 233 @Override 234 public void putOperationChain(OperationChain chain) throws OperationException { 235 putOperationChain(chain, false); 236 } 237 238 final Map<String, OperationType> typeofChains = new HashMap<>(); 239 240 @Override 241 public void putOperationChain(OperationChain chain, boolean replace) throws OperationException { 242 final OperationType typeof = OperationType.typeof(chain, replace); 243 this.putOperation(typeof, replace); 244 typeofChains.put(chain.getId(), typeof); 245 } 246 247 @Override 248 public void removeOperationChain(String id) { 249 OperationType typeof = operations.lookup().get(id); 250 if (typeof == null) { 251 throw new IllegalArgumentException("no such chain " + id); 252 } 253 this.removeOperation(typeof); 254 } 255 256 @Override 257 public OperationChain getOperationChain(String id) throws OperationNotFoundException { 258 OperationType type = getOperation(id); 259 if (type instanceof ChainTypeImpl) { 260 return ((ChainTypeImpl) type).chain; 261 } 262 OperationChain chain = new OperationChain(id); 263 chain.add(id); 264 return chain; 265 } 266 267 @Override 268 public List<OperationChain> getOperationChains() { 269 List<ChainTypeImpl> chainsType = new ArrayList<>(); 270 List<OperationChain> chains = new ArrayList<>(); 271 for (OperationType operationType : operations.lookup().values()) { 272 if (operationType instanceof ChainTypeImpl) { 273 chainsType.add((ChainTypeImpl) operationType); 274 } 275 } 276 for (ChainTypeImpl chainType : chainsType) { 277 chains.add(chainType.getChain()); 278 } 279 return chains; 280 } 281 282 @Override 283 public synchronized void flushCompiledChains() { 284 compiler.cache.invalidateAll(); 285 } 286 287 @Override 288 public void putOperation(Class<?> type) throws OperationException { 289 OperationTypeImpl op = new OperationTypeImpl(this, type); 290 putOperation(op, false); 291 } 292 293 @Override 294 public void putOperation(Class<?> type, boolean replace) throws OperationException { 295 putOperation(type, replace, null); 296 } 297 298 @Override 299 public void putOperation(Class<?> type, boolean replace, String contributingComponent) throws OperationException { 300 OperationTypeImpl op = new OperationTypeImpl(this, type, contributingComponent); 301 putOperation(op, replace); 302 } 303 304 @Override 305 public void putOperation(Class<?> type, boolean replace, String contributingComponent, 306 List<WidgetDefinition> widgetDefinitionList) throws OperationException { 307 OperationTypeImpl op = new OperationTypeImpl(this, type, contributingComponent, widgetDefinitionList); 308 putOperation(op, replace); 309 } 310 311 @Override 312 public void putOperation(OperationType op, boolean replace) throws OperationException { 313 operations.addContribution(op, replace); 314 } 315 316 @Override 317 public void removeOperation(Class<?> key) { 318 OperationType type = operations.getOperationType(key); 319 if (type == null) { 320 log.warn("Cannot remove operation, no such operation " + key); 321 return; 322 } 323 removeOperation(type); 324 } 325 326 @Override 327 public void removeOperation(OperationType type) { 328 operations.removeContribution(type); 329 } 330 331 @Override 332 public OperationType[] getOperations() { 333 HashSet<OperationType> values = new HashSet<>(operations.lookup().values()); 334 return values.toArray(new OperationType[values.size()]); 335 } 336 337 @Override 338 public OperationType getOperation(String id) throws OperationNotFoundException { 339 OperationType op = operations.lookup().get(id); 340 if (op == null) { 341 throw new OperationNotFoundException("No operation was bound on ID: " + id); 342 } 343 return op; 344 } 345 346 /** 347 * @since 5.7.2 348 * @param id operation ID. 349 * @return true if operation registry contains the given operation. 350 */ 351 @Override 352 public boolean hasOperation(String id) { 353 OperationType op = operations.lookup().get(id); 354 return op != null; 355 } 356 357 @Override 358 public CompiledChain compileChain(Class<?> inputType, OperationParameters... ops) throws OperationException { 359 return compileChain(inputType, new OperationChain("", Arrays.asList(ops))); 360 } 361 362 @Override 363 public CompiledChain compileChain(Class<?> inputType, OperationChain chain) throws OperationException { 364 return compiler.compile(ChainTypeImpl.typeof(chain, false), inputType); 365 } 366 367 @Override 368 public void putTypeAdapter(Class<?> accept, Class<?> produce, TypeAdapter adapter) { 369 adapters.put(new TypeAdapterKey(accept, produce), adapter); 370 } 371 372 @Override 373 public void removeTypeAdapter(Class<?> accept, Class<?> produce) { 374 adapters.remove(new TypeAdapterKey(accept, produce)); 375 } 376 377 @Override 378 public TypeAdapter getTypeAdapter(Class<?> accept, Class<?> produce) { 379 return adapters.get(new TypeAdapterKey(accept, produce)); 380 } 381 382 @Override 383 public boolean isTypeAdaptable(Class<?> typeToAdapt, Class<?> targetType) { 384 return getTypeAdapter(typeToAdapt, targetType) != null; 385 } 386 387 @Override 388 @SuppressWarnings("unchecked") 389 public <T> T getAdaptedValue(OperationContext ctx, Object toAdapt, Class<?> targetType) throws OperationException { 390 if (targetType.isAssignableFrom(Void.class)) { 391 return null; 392 } 393 if (OperationContext.class.isAssignableFrom(targetType)) { 394 return (T) ctx; 395 } 396 // handle primitive types 397 Class<?> toAdaptClass = toAdapt == null ? Void.class : toAdapt.getClass(); 398 if (targetType.isPrimitive()) { 399 targetType = getTypeForPrimitive(targetType); 400 if (targetType.isAssignableFrom(toAdaptClass)) { 401 return (T) toAdapt; 402 } 403 } 404 if (targetType.isArray() && toAdapt instanceof List) { 405 @SuppressWarnings("rawtypes") 406 final Iterable iterable = (Iterable) toAdapt; 407 return (T) Iterables.toArray(iterable, targetType.getComponentType()); 408 } 409 TypeAdapter adapter = getTypeAdapter(toAdaptClass, targetType); 410 if (adapter == null) { 411 if (toAdapt == null) { 412 return null; 413 } 414 if (toAdapt instanceof JsonNode) { 415 // fall-back to generic jackson adapter 416 ObjectMapper mapper = new ObjectMapper(); 417 return (T) mapper.convertValue(toAdapt, targetType); 418 } 419 if (targetType.isAssignableFrom(OperationContext.class)) { 420 return (T) ctx; 421 } 422 throw new OperationException( 423 "No type adapter found for input: " + toAdaptClass + " and output " + targetType); 424 } 425 return (T) adapter.getAdaptedValue(ctx, toAdapt); 426 } 427 428 @Override 429 public List<OperationDocumentation> getDocumentation() throws OperationException { 430 List<OperationDocumentation> result = new ArrayList<>(); 431 HashSet<OperationType> ops = new HashSet<>(operations.lookup().values()); 432 ConfigurationService configurationService = Framework.getService(ConfigurationService.class); 433 boolean exportAliases = configurationService.isBooleanPropertyTrue(EXPORT_ALIASES_CONFIGURATION_PARAM); 434 for (OperationType ot : ops.toArray(new OperationType[ops.size()])) { 435 try { 436 OperationDocumentation documentation = ot.getDocumentation(); 437 result.add(documentation); 438 439 // we may want to add an operation documentation for each alias to be backward compatible with old 440 // automation clients 441 String[] aliases = ot.getAliases(); 442 if (exportAliases && aliases != null && aliases.length > 0) { 443 for (String alias : aliases) { 444 result.add(OperationDocumentation.copyForAlias(documentation, alias)); 445 } 446 } 447 } catch (OperationNotFoundException e) { 448 // do nothing 449 } 450 } 451 Collections.sort(result); 452 return result; 453 } 454 455 public static Class<?> getTypeForPrimitive(Class<?> primitiveType) { 456 if (primitiveType == Boolean.TYPE) { 457 return Boolean.class; 458 } else if (primitiveType == Integer.TYPE) { 459 return Integer.class; 460 } else if (primitiveType == Long.TYPE) { 461 return Long.class; 462 } else if (primitiveType == Float.TYPE) { 463 return Float.class; 464 } else if (primitiveType == Double.TYPE) { 465 return Double.class; 466 } else if (primitiveType == Character.TYPE) { 467 return Character.class; 468 } else if (primitiveType == Byte.TYPE) { 469 return Byte.class; 470 } else if (primitiveType == Short.TYPE) { 471 return Short.class; 472 } 473 return primitiveType; 474 } 475 476 /** 477 * @since 5.7.3 478 */ 479 @Override 480 public void putChainException(ChainException exceptionChain) { 481 chainExceptionRegistry.addContribution(exceptionChain); 482 } 483 484 /** 485 * @since 5.7.3 486 */ 487 @Override 488 public void removeExceptionChain(ChainException exceptionChain) { 489 chainExceptionRegistry.removeContribution(exceptionChain); 490 } 491 492 /** 493 * @since 5.7.3 494 */ 495 @Override 496 public ChainException[] getChainExceptions() { 497 Collection<ChainException> chainExceptions = chainExceptionRegistry.lookup().values(); 498 return chainExceptions.toArray(new ChainException[chainExceptions.size()]); 499 } 500 501 /** 502 * @since 5.7.3 503 */ 504 @Override 505 public ChainException getChainException(String onChainId) { 506 return chainExceptionRegistry.getChainException(onChainId); 507 } 508 509 /** 510 * @since 5.7.3 511 */ 512 @Override 513 public boolean hasChainException(String onChainId) { 514 return chainExceptionRegistry.getChainException(onChainId) != null; 515 } 516 517 /** 518 * @since 5.7.3 519 */ 520 @Override 521 public void putAutomationFilter(AutomationFilter automationFilter) { 522 automationFilterRegistry.addContribution(automationFilter); 523 } 524 525 /** 526 * @since 5.7.3 527 */ 528 @Override 529 public void removeAutomationFilter(AutomationFilter automationFilter) { 530 automationFilterRegistry.removeContribution(automationFilter); 531 } 532 533 /** 534 * @since 5.7.3 535 */ 536 @Override 537 public AutomationFilter getAutomationFilter(String id) { 538 return automationFilterRegistry.getAutomationFilter(id); 539 } 540 541 /** 542 * @since 5.7.3 543 */ 544 @Override 545 public AutomationFilter[] getAutomationFilters() { 546 Collection<AutomationFilter> automationFilters = automationFilterRegistry.lookup().values(); 547 return automationFilters.toArray(new AutomationFilter[automationFilters.size()]); 548 } 549 550}