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