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}