/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.isis.core.metamodel.facets.actions.action.invocation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.sql.Timestamp; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.concurrent.Callable; import com.google.common.base.Strings; import com.google.common.collect.Lists; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.isis.applib.NonRecoverableException; import org.apache.isis.applib.RecoverableException; import org.apache.isis.applib.services.bookmark.Bookmark; import org.apache.isis.applib.services.bookmark.BookmarkService; import org.apache.isis.applib.services.clock.ClockService; import org.apache.isis.applib.services.command.Command; import org.apache.isis.applib.services.command.CommandContext; import org.apache.isis.applib.services.command.spi.CommandService; import org.apache.isis.applib.services.eventbus.AbstractDomainEvent; import org.apache.isis.applib.services.eventbus.ActionDomainEvent; import org.apache.isis.applib.services.iactn.Interaction; import org.apache.isis.applib.services.iactn.InteractionContext; import org.apache.isis.applib.services.metamodel.MetaModelService2; import org.apache.isis.applib.services.queryresultscache.QueryResultsCache; import org.apache.isis.applib.services.repository.RepositoryService; import org.apache.isis.applib.services.xactn.TransactionService; import org.apache.isis.applib.services.xactn.TransactionState; import org.apache.isis.core.commons.authentication.AuthenticationSession; import org.apache.isis.core.commons.authentication.AuthenticationSessionProvider; import org.apache.isis.core.commons.config.IsisConfiguration; import org.apache.isis.core.commons.exceptions.IsisException; import org.apache.isis.core.commons.lang.ArrayExtensions; import org.apache.isis.core.commons.lang.ThrowableExtensions; import org.apache.isis.core.metamodel.adapter.ObjectAdapter; import org.apache.isis.core.metamodel.consent.InteractionInitiatedBy; import org.apache.isis.core.metamodel.deployment.DeploymentCategory; import org.apache.isis.core.metamodel.facetapi.FacetHolder; import org.apache.isis.core.metamodel.facetapi.IdentifiedHolder; import org.apache.isis.core.metamodel.facets.CollectionUtils; import org.apache.isis.core.metamodel.facets.DomainEventHelper; import org.apache.isis.core.metamodel.facets.ImperativeFacet; import org.apache.isis.core.metamodel.facets.actcoll.typeof.ElementSpecificationProviderFromTypeOfFacet; import org.apache.isis.core.metamodel.facets.actcoll.typeof.TypeOfFacet; import org.apache.isis.core.metamodel.facets.actions.publish.PublishedActionFacet; import org.apache.isis.core.metamodel.facets.actions.semantics.ActionSemanticsFacet; import org.apache.isis.core.metamodel.facets.collections.modify.CollectionFacet; import org.apache.isis.core.metamodel.facets.object.viewmodel.ViewModelFacet; import org.apache.isis.core.metamodel.services.ServicesInjector; import org.apache.isis.core.metamodel.services.ixn.InteractionDtoServiceInternal; import org.apache.isis.core.metamodel.services.persistsession.PersistenceSessionServiceInternal; import org.apache.isis.core.metamodel.services.publishing.PublishingServiceInternal; import org.apache.isis.core.metamodel.spec.ObjectSpecification; import org.apache.isis.core.metamodel.spec.feature.Contributed; import org.apache.isis.core.metamodel.spec.feature.ObjectAction; import org.apache.isis.core.metamodel.specloader.ReflectiveActionException; import org.apache.isis.core.metamodel.specloader.specimpl.MixedInMember2; import org.apache.isis.core.runtime.system.transaction.TransactionalClosure; import org.apache.isis.schema.ixn.v1.ActionInvocationDto; public abstract class ActionInvocationFacetForDomainEventAbstract extends ActionInvocationFacetAbstract implements ImperativeFacet { private final static Logger LOG = LoggerFactory.getLogger(ActionInvocationFacetForDomainEventAbstract.class); private final Method method; private final ObjectSpecification onType; private final ObjectSpecification returnType; private final PersistenceSessionServiceInternal persistenceSessionServiceInternal; private final DeploymentCategory deploymentCategory; private final AuthenticationSessionProvider authenticationSessionProvider; private final ServicesInjector servicesInjector; private final IsisConfiguration configuration; private final Class<? extends ActionDomainEvent<?>> eventType; private final DomainEventHelper domainEventHelper; public ActionInvocationFacetForDomainEventAbstract( final Class<? extends ActionDomainEvent<?>> eventType, final Method method, final ObjectSpecification onType, final ObjectSpecification returnType, final FacetHolder holder, final ServicesInjector servicesInjector) { super(holder); this.eventType = eventType; this.method = method; this.onType = onType; this.returnType = returnType; this.deploymentCategory = servicesInjector.getDeploymentCategoryProvider().getDeploymentCategory(); this.authenticationSessionProvider = servicesInjector.getAuthenticationSessionProvider(); this.persistenceSessionServiceInternal = servicesInjector.getPersistenceSessionServiceInternal(); this.servicesInjector = servicesInjector; this.configuration = servicesInjector.getConfigurationServiceInternal(); this.domainEventHelper = new DomainEventHelper(this.servicesInjector); } /** * Returns a singleton list of the {@link java.lang.reflect.Method} provided in the * constructor. */ @Override public List<Method> getMethods() { return Collections.singletonList(method); } @Override public Intent getIntent(final Method method) { return Intent.EXECUTE; } @Override public ObjectSpecification getReturnType() { return returnType; } @Override public ObjectSpecification getOnType() { return onType; } @Override public ObjectAdapter invoke( final ObjectAction owningAction, final ObjectAdapter targetAdapter, final ObjectAdapter mixedInAdapter, final ObjectAdapter[] argumentAdapters, final InteractionInitiatedBy interactionInitiatedBy) { final ObjectAdapter[] holder = new ObjectAdapter[1]; getPersistenceSessionServiceInternal().executeWithinTransaction( new TransactionalClosure(){ @Override public void execute() { holder[0] = doInvoke(owningAction, targetAdapter, mixedInAdapter, argumentAdapters, interactionInitiatedBy); } } ); return holder[0]; } ObjectAdapter doInvoke( final ObjectAction owningAction, final ObjectAdapter targetAdapter, final ObjectAdapter mixedInAdapter, final ObjectAdapter[] argumentAdapters, final InteractionInitiatedBy interactionInitiatedBy) { // similar code in PropertySetterOrClearFacetFDEA final CommandContext commandContext = getCommandContext(); final Command command = commandContext.getCommand(); final InteractionContext interactionContext = getInteractionContext(); final Interaction interaction = interactionContext.getInteraction(); final String actionId = owningAction.getIdentifier().toClassAndNameIdentityString(); final ObjectAdapter returnedAdapter; if( command.getExecutor() == Command.Executor.USER && command.getExecuteIn() == org.apache.isis.applib.annotation.Command.ExecuteIn.BACKGROUND) { // deal with background commands // persist command so can it can subsequently be invoked in the 'background' final CommandService commandService = getCommandService(); if (!commandService.persistIfPossible(command)) { throw new IsisException(String.format( "Unable to persist command for action '%s'; CommandService does not support persistent commands ", actionId)); } returnedAdapter = getPersistenceSessionServiceInternal().adapterFor(command); } else { // otherwise, go ahead and execute action in the 'foreground' final ObjectAdapter mixinElseRegularAdapter = mixedInAdapter != null ? mixedInAdapter : targetAdapter; owningAction.setupBulkActionInvocationContext(mixinElseRegularAdapter); final Object mixinElseRegularPojo = ObjectAdapter.Util.unwrap(mixinElseRegularAdapter); final List<ObjectAdapter> argumentAdapterList = Arrays.asList(argumentAdapters); final List<Object> argumentPojos = ObjectAdapter.Util.unwrap(argumentAdapterList); final String targetMember = targetNameFor(owningAction, mixedInAdapter); final String targetClass = CommandUtil.targetClassNameFor(mixinElseRegularAdapter); final Interaction.ActionInvocation execution = new Interaction.ActionInvocation(interaction, actionId, mixinElseRegularPojo, argumentPojos, targetMember, targetClass); final Interaction.MemberExecutor<Interaction.ActionInvocation> callable = new Interaction.MemberExecutor<Interaction.ActionInvocation>() { @Override public Object execute(final Interaction.ActionInvocation currentExecution) { try { // update the current execution with the DTO (memento) final ActionInvocationDto invocationDto = getInteractionDtoServiceInternal().asActionInvocationDto( owningAction, mixinElseRegularAdapter, argumentAdapterList); currentExecution.setDto(invocationDto); // set the startedAt (and update command if this is the top-most member execution) // (this isn't done within Interaction#execute(...) because it requires the DTO // to have been set on the current execution). final Timestamp startedAt = getClockService().nowAsJavaSqlTimestamp(); execution.setStartedAt(startedAt); if(command.getStartedAt() == null) { command.setStartedAt(startedAt); } // ... post the executing event final ActionDomainEvent<?> event = domainEventHelper.postEventForAction( AbstractDomainEvent.Phase.EXECUTING, eventType, null, owningAction, owningAction, targetAdapter, mixedInAdapter, argumentAdapters, command, null); // set event onto the execution currentExecution.setEvent(event); // invoke method final Object resultPojo = invokeMethodElseFromCache(targetAdapter, argumentAdapters); final ObjectAdapter resultAdapterPossiblyCloned = cloneIfViewModelCloneable(resultPojo, mixinElseRegularAdapter); // ... post the executed event domainEventHelper.postEventForAction( AbstractDomainEvent.Phase.EXECUTED, eventType, verify(event), owningAction, owningAction, targetAdapter, mixedInAdapter, argumentAdapters, command, resultAdapterPossiblyCloned); return ObjectAdapter.Util.unwrap(resultAdapterPossiblyCloned); } catch (IllegalAccessException ex) { throw new ReflectiveActionException("Illegal access of " + method, ex); } catch (InvocationTargetException ex) { final Throwable targetException = ex.getTargetException(); if (targetException instanceof IllegalStateException) { throw new ReflectiveActionException( String.format( "IllegalStateException thrown while executing %s %s", method, targetException.getMessage()), targetException); } if(targetException instanceof RecoverableException) { if (!getTransactionState().canCommit()) { // something severe has happened to the underlying transaction; // so escalate this exception to be non-recoverable final Throwable targetExceptionCause = targetException.getCause(); Throwable nonRecoverableCause = targetExceptionCause != null ? targetExceptionCause : targetException; // trim to first 300 chars final String message = trim(nonRecoverableCause.getMessage(), 300); throw new NonRecoverableException(message, nonRecoverableCause); } } ThrowableExtensions.throwWithinIsisException(ex, "Exception executing " + method); return null; // never executed, previous line throws } } }; // sets up startedAt and completedAt on the execution, also manages the execution call graph interaction.execute(callable, execution); // handle any exceptions final Interaction.Execution<ActionInvocationDto, ?> priorExecution = interaction.getPriorExecution(); final Exception executionExceptionIfAny = priorExecution.getThrew(); // TODO: should also sync DTO's 'threw' attribute here...? if(executionExceptionIfAny != null) { throw executionExceptionIfAny instanceof RuntimeException ? ((RuntimeException)executionExceptionIfAny) : new RuntimeException(executionExceptionIfAny); } final Object returnedPojo = priorExecution.getReturned(); returnedAdapter = persistenceSessionServiceInternal.adapterFor(returnedPojo); // sync DTO with result getInteractionDtoServiceInternal().updateResult(priorExecution.getDto(), owningAction, returnedPojo); // update Command (if required) setCommandResultIfEntity(command, returnedAdapter); // publish (if not a contributed association, query-only mixin) final PublishedActionFacet publishedActionFacet = getIdentified().getFacet(PublishedActionFacet.class); if (publishedActionFacet != null) { final IdentifiedHolder identifiedHolder = getIdentified(); final List<ObjectAdapter> parameterAdapters = Arrays.asList(argumentAdapters); getPublishingServiceInternal().publishAction( priorExecution, owningAction, identifiedHolder, targetAdapter, parameterAdapters, returnedAdapter); } } return filteredIfRequired(returnedAdapter, interactionInitiatedBy); } // TODO: could improve this, currently have to go searching for the mixin private static String targetNameFor(ObjectAction owningAction, ObjectAdapter mixedInAdapter) { if(mixedInAdapter != null) { ObjectSpecification onType = owningAction.getOnType(); ObjectSpecification mixedInSpec = mixedInAdapter.getSpecification(); List<ObjectAction> objectActions1 = mixedInSpec.getObjectActions(Contributed.INCLUDED); for (ObjectAction objectAction : objectActions1) { if(objectAction instanceof MixedInMember2) { MixedInMember2 action = (MixedInMember2) objectAction; if(action.getMixinType() == onType) { return action.getName(); } } } } return CommandUtil.targetMemberNameFor(owningAction); } private static String trim(String message, final int maxLen) { if(!Strings.isNullOrEmpty(message)) { message = message.substring(0, Math.min(message.length(), maxLen)); if(message.length() == maxLen) { message += " ..."; } } return message; } protected Object invokeMethodElseFromCache( final ObjectAdapter targetAdapter, final ObjectAdapter[] arguments) throws IllegalAccessException, InvocationTargetException { final Object[] executionParameters = new Object[arguments.length]; for (int i = 0; i < arguments.length; i++) { executionParameters[i] = unwrap(arguments[i]); } final Object targetPojo = unwrap(targetAdapter); final ActionSemanticsFacet semanticsFacet = getFacetHolder().getFacet(ActionSemanticsFacet.class); final boolean cacheable = semanticsFacet != null && semanticsFacet.value().isSafeAndRequestCacheable(); if(cacheable) { final QueryResultsCache queryResultsCache = getQueryResultsCache(); final Object[] targetPojoPlusExecutionParameters = ArrayExtensions.appendT(executionParameters, targetPojo); return queryResultsCache.execute(new Callable<Object>() { @Override public Object call() throws Exception { return method.invoke(targetPojo, executionParameters); } }, targetPojo.getClass(), method.getName(), targetPojoPlusExecutionParameters); } else { return method.invoke(targetPojo, executionParameters); } } protected ObjectAdapter cloneIfViewModelCloneable( final Object resultPojo, final ObjectAdapter targetAdapter) { // to remove boilerplate from the domain, we automatically clone the returned object if it is a view model. if (resultPojo != null) { final ObjectAdapter resultAdapter = getPersistenceSessionServiceInternal().adapterFor(resultPojo); return cloneIfViewModelElse(resultAdapter, resultAdapter); } else { // if void or null, attempt to clone the original target, else return null. return cloneIfViewModelElse(targetAdapter, null); } } private ObjectAdapter cloneIfViewModelElse(final ObjectAdapter adapter, final ObjectAdapter dfltAdapter) { if (!adapter.getSpecification().isViewModelCloneable(adapter)) { return dfltAdapter; } final ViewModelFacet viewModelFacet = adapter.getSpecification().getFacet(ViewModelFacet.class); final Object clone = viewModelFacet.clone(adapter.getObject()); final ObjectAdapter clonedAdapter = getPersistenceSessionServiceInternal().adapterFor(clone); // copy over TypeOfFacet if required final TypeOfFacet typeOfFacet = getFacetHolder().getFacet(TypeOfFacet.class); clonedAdapter.setElementSpecificationProvider(ElementSpecificationProviderFromTypeOfFacet.createFrom(typeOfFacet)); return clonedAdapter; } protected void setCommandResultIfEntity(final Command command, final ObjectAdapter resultAdapter) { if(command.getResult() != null) { // don't trample over any existing result, eg subsequent mixins. return; } if (resultAdapter == null) { return; } final Class<?> domainType = resultAdapter.getSpecification().getCorrespondingClass(); final MetaModelService2.Sort sort = getMetaModelService().sortOf(domainType); switch (sort) { case JDO_ENTITY: final Object domainObject = resultAdapter.getObject(); // ensure that any still-to-be-persisted adapters get persisted to DB. if(!getRepositoryService().isPersistent(domainObject)) { getTransactionService().flushTransaction(); } if(getRepositoryService().isPersistent(domainObject)) { BookmarkService bookmarkService = getBookmarkService(); Bookmark bookmark = bookmarkService.bookmarkFor(domainObject); command.setResult(bookmark); } break; default: // ignore all other sorts of objects break; } } private MetaModelService2 getMetaModelService() { return servicesInjector.lookupServiceElseFail(MetaModelService2.class); } private TransactionService getTransactionService() { return servicesInjector.lookupServiceElseFail(TransactionService.class); } private BookmarkService getBookmarkService() { return servicesInjector.lookupServiceElseFail(BookmarkService.class); } private RepositoryService getRepositoryService() { return servicesInjector.lookupServiceElseFail(RepositoryService.class); } protected ObjectAdapter filteredIfRequired( final ObjectAdapter resultAdapter, final InteractionInitiatedBy interactionInitiatedBy) { if (resultAdapter == null) { return null; } final boolean filterForVisibility = getConfiguration().getBoolean("isis.reflector.facet.filterVisibility", true); if (!filterForVisibility) { return resultAdapter; } final Object result = resultAdapter.getObject(); if(result instanceof Collection || result.getClass().isArray()) { final CollectionFacet facet = CollectionFacet.Utils.getCollectionFacetFromSpec(resultAdapter); final Iterable<ObjectAdapter> adapterList = facet.iterable(resultAdapter); final List<ObjectAdapter> visibleAdapters = ObjectAdapter.Util.visibleAdapters( adapterList, interactionInitiatedBy); final Object visibleObjects = CollectionUtils.copyOf( Lists.transform(visibleAdapters, ObjectAdapter.Functions.getObject()), method.getReturnType()); if (visibleObjects != null) { return getPersistenceSessionServiceInternal().adapterFor(visibleObjects); } // would be null if unable to take a copy (unrecognized return type) // fallback to returning the original adapter, without filtering for visibility return resultAdapter; } else { boolean visible = ObjectAdapter.Util.isVisible(resultAdapter, interactionInitiatedBy); return visible ? resultAdapter : null; } } /** * Optional hook to allow the facet implementation for the deprecated {@link org.apache.isis.applib.annotation.PostsActionInvokedEvent} annotation * to discard the event if the domain event is of a different type (specifically if was installed by virtue of a no * @{@link org.apache.isis.applib.annotation.Action} or @{@link org.apache.isis.applib.annotation.ActionInteraction} annotations. */ protected ActionDomainEvent<?> verify(final ActionDomainEvent<?> event) { return event; } /** * For testing only. */ public Class<? extends ActionDomainEvent<?>> getEventType() { return eventType; } private static Object unwrap(final ObjectAdapter adapter) { return adapter == null ? null : adapter.getObject(); } @Override protected String toStringValues() { return "method=" + method; } // ///////////////////////////////////////////////////////// // Dependencies (looked up) // ///////////////////////////////////////////////////////// private CommandContext getCommandContext() { return servicesInjector.lookupServiceElseFail(CommandContext.class); } private InteractionContext getInteractionContext() { return servicesInjector.lookupServiceElseFail(InteractionContext.class); } private QueryResultsCache getQueryResultsCache() { return servicesInjector.lookupServiceElseFail(QueryResultsCache.class); } private CommandService getCommandService() { return servicesInjector.lookupServiceElseFail(CommandService.class); } private ClockService getClockService() { return servicesInjector.lookupServiceElseFail(ClockService.class); } private PublishingServiceInternal getPublishingServiceInternal() { return servicesInjector.lookupServiceElseFail(PublishingServiceInternal.class); } private InteractionDtoServiceInternal getInteractionDtoServiceInternal() { return servicesInjector.lookupServiceElseFail(InteractionDtoServiceInternal.class); } // ///////////////////////////////////////////////////////// // Dependencies (from constructor) // ///////////////////////////////////////////////////////// private PersistenceSessionServiceInternal getPersistenceSessionServiceInternal() { return persistenceSessionServiceInternal; } public IsisConfiguration getConfiguration() { return configuration; } public DeploymentCategory getDeploymentCategory() { return deploymentCategory; } public AuthenticationSession getAuthenticationSession() { return authenticationSessionProvider.getAuthenticationSession(); } public TransactionState getTransactionState() { return persistenceSessionServiceInternal.getTransactionState(); } }