001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019 020package org.apache.isis.core.progmodel.facets.actions.invoke; 021 022import java.lang.reflect.InvocationTargetException; 023import java.lang.reflect.Method; 024import java.util.Collections; 025import java.util.List; 026 027import org.slf4j.Logger; 028import org.slf4j.LoggerFactory; 029 030import org.apache.isis.applib.NonRecoverableException; 031import org.apache.isis.applib.RecoverableException; 032import org.apache.isis.applib.annotation.Bulk; 033import org.apache.isis.applib.annotation.Bulk.InteractionContext.InvokedAs; 034import org.apache.isis.applib.annotation.Command.ExecuteIn; 035import org.apache.isis.applib.annotation.Command.Persistence; 036import org.apache.isis.applib.clock.Clock; 037import org.apache.isis.applib.services.background.ActionInvocationMemento; 038import org.apache.isis.applib.services.background.BackgroundService; 039import org.apache.isis.applib.services.bookmark.Bookmark; 040import org.apache.isis.applib.services.command.Command; 041import org.apache.isis.applib.services.command.Command.Executor; 042import org.apache.isis.applib.services.command.CommandContext; 043import org.apache.isis.applib.services.command.spi.CommandService; 044import org.apache.isis.core.commons.exceptions.IsisException; 045import org.apache.isis.core.commons.lang.ThrowableExtensions; 046import org.apache.isis.core.metamodel.adapter.ObjectAdapter; 047import org.apache.isis.core.metamodel.adapter.mgr.AdapterManager; 048import org.apache.isis.core.metamodel.facetapi.FacetHolder; 049import org.apache.isis.core.metamodel.facets.ImperativeFacet; 050import org.apache.isis.core.metamodel.facets.ImperativeFacet.Intent; 051import org.apache.isis.core.metamodel.facets.actions.command.CommandFacet; 052import org.apache.isis.core.metamodel.facets.actions.invoke.ActionInvocationFacet; 053import org.apache.isis.core.metamodel.facets.actions.invoke.ActionInvocationFacetAbstract; 054import org.apache.isis.core.metamodel.facets.actions.publish.PublishedActionFacet; 055import org.apache.isis.core.metamodel.facets.object.viewmodel.ViewModelFacet; 056import org.apache.isis.core.metamodel.facets.typeof.ElementSpecificationProviderFromTypeOfFacet; 057import org.apache.isis.core.metamodel.facets.typeof.TypeOfFacet; 058import org.apache.isis.core.metamodel.runtimecontext.RuntimeContext; 059import org.apache.isis.core.metamodel.runtimecontext.ServicesInjector; 060import org.apache.isis.core.metamodel.spec.ObjectSpecification; 061import org.apache.isis.core.metamodel.spec.feature.ObjectAction; 062import org.apache.isis.core.metamodel.specloader.ReflectiveActionException; 063import org.apache.isis.core.progmodel.facets.actions.bulk.BulkFacet; 064 065public class ActionInvocationFacetViaMethod extends ActionInvocationFacetAbstract implements ImperativeFacet { 066 067 private final static Logger LOG = LoggerFactory.getLogger(ActionInvocationFacetViaMethod.class); 068 069 private final Method method; 070 private final ObjectSpecification onType; 071 private final ObjectSpecification returnType; 072 073 private final AdapterManager adapterManager; 074 private final ServicesInjector servicesInjector; 075 private final RuntimeContext runtimeContext; 076 077 public ActionInvocationFacetViaMethod( 078 final Method method, 079 final ObjectSpecification onType, 080 final ObjectSpecification returnType, 081 final FacetHolder holder, 082 final RuntimeContext runtimeContext, 083 final AdapterManager adapterManager, 084 final ServicesInjector servicesInjector) { 085 super(holder); 086 this.method = method; 087 this.onType = onType; 088 this.returnType = returnType; 089 this.runtimeContext = runtimeContext; 090 this.adapterManager = adapterManager; 091 this.servicesInjector = servicesInjector; 092 } 093 094 /** 095 * Returns a singleton list of the {@link Method} provided in the 096 * constructor. 097 */ 098 @Override 099 public List<Method> getMethods() { 100 return Collections.singletonList(method); 101 } 102 103 @Override 104 public Intent getIntent(final Method method) { 105 return Intent.EXECUTE; 106 } 107 108 @Override 109 public ObjectSpecification getReturnType() { 110 return returnType; 111 } 112 113 @Override 114 public ObjectSpecification getOnType() { 115 return onType; 116 } 117 118 @Override 119 public ObjectAdapter invoke(final ObjectAdapter target, final ObjectAdapter[] parameters) { 120 return invoke(null, target, parameters); 121 } 122 123 @Override 124 public ObjectAdapter invoke( 125 final ObjectAction owningAction, 126 final ObjectAdapter targetAdapter, 127 final ObjectAdapter[] arguments) { 128 129 // Can return null both because the action finally was not invoked 130 // or because it returned null. 131 return internalInvoke(owningAction, targetAdapter, arguments).getAdapter(); 132 133 } 134 135 /** 136 * Introduced to disambiguate the meaning of <tt>null</tt> as a return value of 137 * {@link ActionInvocationFacetViaMethod#invoke(ObjectAdapter, ObjectAdapter[])} 138 */ 139 public static class InvocationResult { 140 141 public static InvocationResult forActionThatReturned(final ObjectAdapter resultAdapter) { 142 return new InvocationResult(true, resultAdapter); 143 } 144 145 public static InvocationResult forActionNotInvoked() { 146 return new InvocationResult(false, null); 147 } 148 149 private final boolean whetherInvoked; 150 private final ObjectAdapter adapter; 151 152 private InvocationResult(final boolean whetherInvoked, final ObjectAdapter result) { 153 this.whetherInvoked = whetherInvoked; 154 this.adapter = result; 155 } 156 157 public boolean getWhetherInvoked() { 158 return whetherInvoked; 159 } 160 161 /** 162 * Returns the result, or null if either the action invocation returned null or 163 * if the action was never invoked in the first place. 164 * 165 * <p> 166 * Use {@link #getWhetherInvoked()} to distinguish between these two cases. 167 */ 168 public ObjectAdapter getAdapter() { 169 return adapter; 170 } 171 } 172 173 protected InvocationResult internalInvoke( 174 final ObjectAction owningAction, 175 final ObjectAdapter targetAdapter, 176 final ObjectAdapter[] arguments) { 177 178 final Bulk.InteractionContext bulkInteractionContext = getServicesInjector().lookupService(Bulk.InteractionContext.class); 179 final CommandContext commandContext = getServicesInjector().lookupService(CommandContext.class); 180 final Command command = commandContext != null ? commandContext.getCommand() : null; 181 182 try { 183 final Object[] executionParameters = new Object[arguments.length]; 184 for (int i = 0; i < arguments.length; i++) { 185 executionParameters[i] = unwrap(arguments[i]); 186 } 187 188 final Object object = unwrap(targetAdapter); 189 190 final BulkFacet bulkFacet = getFacetHolder().getFacet(BulkFacet.class); 191 if (bulkFacet != null && 192 bulkInteractionContext != null && 193 bulkInteractionContext.getInvokedAs() == null) { 194 195 bulkInteractionContext.setInvokedAs(InvokedAs.REGULAR); 196 bulkInteractionContext.setDomainObjects(Collections.singletonList(object)); 197 } 198 199 if(command != null && command.getExecutor() == Executor.USER && owningAction != null) { 200 201 if(command.getTarget() != null) { 202 // already set up by a ObjectActionContributee; 203 // don't overwrite 204 } else { 205 command.setTargetClass(CommandUtil.targetClassNameFor(targetAdapter)); 206 command.setTargetAction(CommandUtil.targetActionNameFor(owningAction)); 207 command.setArguments(CommandUtil.argDescriptionFor(owningAction, arguments)); 208 209 final Bookmark targetBookmark = CommandUtil.bookmarkFor(targetAdapter); 210 command.setTarget(targetBookmark); 211 } 212 213 command.setMemberIdentifier(CommandUtil.actionIdentifierFor(owningAction)); 214 215 // the background service is used here merely as a means to capture an invocation memento 216 final BackgroundService backgroundService = getServicesInjector().lookupService(BackgroundService.class); 217 if(backgroundService != null) { 218 final Object targetObject = unwrap(targetAdapter); 219 final Object[] args = CommandUtil.objectsFor(arguments); 220 ActionInvocationMemento aim = backgroundService.asActionInvocationMemento(method, targetObject, args); 221 222 if(aim != null) { 223 command.setMemento(aim.asMementoString()); 224 } else { 225 throw new IsisException( 226 "Unable to build memento for action " + 227 owningAction.getIdentifier().toClassAndNameIdentityString()); 228 } 229 } 230 231 // copy over the command execution 'context' (if available) 232 final CommandFacet commandFacet = getFacetHolder().getFacet(CommandFacet.class); 233 if(commandFacet != null && !commandFacet.isDisabled()) { 234 command.setExecuteIn(commandFacet.executeIn()); 235 command.setPersistence(commandFacet.persistence()); 236 } else { 237 // if no facet, assume do want to execute right now, but only persist (eventually) if hinted. 238 command.setExecuteIn(ExecuteIn.FOREGROUND); 239 command.setPersistence(Persistence.IF_HINTED); 240 } 241 } 242 243 244 if( command != null && 245 command.getExecutor() == Executor.USER && 246 command.getExecuteIn() == ExecuteIn.BACKGROUND) { 247 248 // persist command so can be this command can be in the 'background' 249 final CommandService commandService = getServicesInjector().lookupService(CommandService.class); 250 if(commandService.persistIfPossible(command)) { 251 // force persistence, then return the command itself. 252 final ObjectAdapter resultAdapter = getAdapterManager().adapterFor(command); 253 return InvocationResult.forActionThatReturned(resultAdapter); 254 } else { 255 throw new IsisException( 256 "Unable to schedule action '" 257 + owningAction.getIdentifier().toClassAndNameIdentityString() + "' to run in background: " 258 + "CommandService does not support persistent commands " ); 259 } 260 } else { 261 262 // otherwise, go ahead and execute action in the 'foreground' 263 264 if(command != null) { 265 command.setStartedAt(Clock.getTimeAsJavaSqlTimestamp()); 266 } 267 268 final Object result = method.invoke(object, executionParameters); 269 270 if (LOG.isDebugEnabled()) { 271 LOG.debug(" action result " + result); 272 } 273 if (result == null) { 274 return InvocationResult.forActionThatReturned(null); 275 } 276 277 final ObjectAdapter resultAdapter = getAdapterManager().adapterFor(result); 278 279 // copy over TypeOfFacet if required 280 final TypeOfFacet typeOfFacet = getFacetHolder().getFacet(TypeOfFacet.class); 281 resultAdapter.setElementSpecificationProvider(ElementSpecificationProviderFromTypeOfFacet.createFrom(typeOfFacet)); 282 283 if(command != null) { 284 if(!resultAdapter.getSpecification().containsDoOpFacet(ViewModelFacet.class)) { 285 final Bookmark bookmark = CommandUtil.bookmarkFor(resultAdapter); 286 command.setResult(bookmark); 287 } 288 } 289 290 final PublishedActionFacet publishedActionFacet = getIdentified().getFacet(PublishedActionFacet.class); 291 ActionInvocationFacet.currentInvocation.set( 292 publishedActionFacet != null 293 ? new CurrentInvocation(targetAdapter, getIdentified(), arguments, resultAdapter, command) 294 :null); 295 296 return InvocationResult.forActionThatReturned(resultAdapter); 297 } 298 299 } catch (final IllegalArgumentException e) { 300 throw e; 301 } catch (final InvocationTargetException e) { 302 final Throwable targetException = e.getTargetException(); 303 if (targetException instanceof IllegalStateException) { 304 throw new ReflectiveActionException("IllegalStateException thrown while executing " + method + " " + targetException.getMessage(), targetException); 305 } 306 if(targetException instanceof RecoverableException) { 307 if (!runtimeContext.getTransactionState().canCommit()) { 308 // something severe has happened to the underlying transaction; 309 // so escalate this exception to be non-recoverable 310 final Throwable targetExceptionCause = targetException.getCause(); 311 Throwable nonRecoverableCause = targetExceptionCause != null? targetExceptionCause: targetException; 312 throw new NonRecoverableException(nonRecoverableCause); 313 } 314 } 315 316 ThrowableExtensions.throwWithinIsisException(e, "Exception executing " + method); 317 318 // Action was not invoked (an Exception was thrown) 319 return InvocationResult.forActionNotInvoked(); 320 } catch (final IllegalAccessException e) { 321 throw new ReflectiveActionException("Illegal access of " + method, e); 322 } 323 } 324 325 private static Object unwrap(final ObjectAdapter adapter) { 326 return adapter == null ? null : adapter.getObject(); 327 } 328 329 @Override 330 public boolean impliesResolve() { 331 return true; 332 } 333 334 @Override 335 public boolean impliesObjectChanged() { 336 return false; 337 } 338 339 @Override 340 protected String toStringValues() { 341 return "method=" + method; 342 } 343 344 // ///////////////////////////////////////////////////////// 345 // Dependencies (from constructor) 346 // ///////////////////////////////////////////////////////// 347 348 private AdapterManager getAdapterManager() { 349 return adapterManager; 350 } 351 352 private ServicesInjector getServicesInjector() { 353 return servicesInjector; 354 } 355 356 357}