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