/** * 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.runtime.services.background; import java.sql.Timestamp; import java.util.Collections; import java.util.List; import com.google.common.base.Function; import com.google.common.base.Throwables; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import org.apache.isis.applib.services.background.ActionInvocationMemento; import org.apache.isis.applib.services.bookmark.Bookmark; import org.apache.isis.applib.services.bookmark.BookmarkService2; import org.apache.isis.applib.services.clock.ClockService; import org.apache.isis.applib.services.command.Command; import org.apache.isis.applib.services.command.Command.Executor; import org.apache.isis.applib.services.command.CommandContext; import org.apache.isis.applib.services.iactn.Interaction; import org.apache.isis.applib.services.iactn.InteractionContext; import org.apache.isis.applib.services.jaxb.JaxbService; import org.apache.isis.core.metamodel.adapter.ObjectAdapter; import org.apache.isis.core.metamodel.consent.InteractionInitiatedBy; import org.apache.isis.core.metamodel.facets.actions.action.invocation.CommandUtil; 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.spec.feature.ObjectAssociation; import org.apache.isis.core.metamodel.spec.feature.OneToOneAssociation; import org.apache.isis.core.runtime.services.memento.MementoServiceDefault; import org.apache.isis.core.runtime.sessiontemplate.AbstractIsisSessionTemplate; import org.apache.isis.core.runtime.system.persistence.PersistenceSession; import org.apache.isis.core.runtime.system.transaction.IsisTransactionManager; import org.apache.isis.core.runtime.system.transaction.TransactionalClosure; import org.apache.isis.schema.cmd.v1.ActionDto; import org.apache.isis.schema.cmd.v1.CommandDto; import org.apache.isis.schema.cmd.v1.MemberDto; import org.apache.isis.schema.cmd.v1.ParamDto; import org.apache.isis.schema.cmd.v1.ParamsDto; import org.apache.isis.schema.cmd.v1.PropertyDto; import org.apache.isis.schema.common.v1.InteractionType; import org.apache.isis.schema.common.v1.OidDto; import org.apache.isis.schema.common.v1.OidsDto; import org.apache.isis.schema.common.v1.ValueWithTypeDto; import org.apache.isis.schema.utils.CommandDtoUtils; import org.apache.isis.schema.utils.CommonDtoUtils; /** * Intended to be used as a base class for executing queued up {@link Command background action}s. * * <p> * This implementation uses the {@link #findBackgroundCommandsToExecute() hook method} so that it is * independent of the location where the actions have actually been persisted to. */ public abstract class BackgroundCommandExecution extends AbstractIsisSessionTemplate { private final MementoServiceDefault mementoService; public BackgroundCommandExecution() { // same as configured by BackgroundServiceDefault mementoService = new MementoServiceDefault().withNoEncoding(); } // ////////////////////////////////////// protected void doExecute(Object context) { final PersistenceSession persistenceSession = getPersistenceSession(); final IsisTransactionManager transactionManager = getTransactionManager(persistenceSession); final List<Command> backgroundCommands = Lists.newArrayList(); transactionManager.executeWithinTransaction(new TransactionalClosure() { @Override public void execute() { backgroundCommands.addAll(findBackgroundCommandsToExecute()); } }); for (final Command backgroundCommand : backgroundCommands) { execute(transactionManager, backgroundCommand); } } /** * Mandatory hook method */ protected abstract List<? extends Command> findBackgroundCommandsToExecute(); // ////////////////////////////////////// private void execute( final IsisTransactionManager transactionManager, final Command backgroundCommand) { transactionManager.executeWithinTransaction( backgroundCommand, new TransactionalClosure() { @Override public void execute() { // setup for us by IsisTransactionManager; will have the transactionId of the backgroundCommand final Interaction backgroundInteraction = interactionContext.getInteraction(); final String memento = backgroundCommand.getMemento(); try { backgroundCommand.setExecutor(Executor.BACKGROUND); final boolean legacy = memento.startsWith("<memento"); if(legacy) { final ActionInvocationMemento aim = new ActionInvocationMemento(mementoService, memento); final String actionId = aim.getActionId(); final Bookmark targetBookmark = aim.getTarget(); final Object targetObject = bookmarkService.lookup( targetBookmark, BookmarkService2.FieldResetPolicy.RESET); final ObjectAdapter targetAdapter = adapterFor(targetObject); final ObjectSpecification specification = targetAdapter.getSpecification(); final ObjectAction objectAction = findActionElseNull(specification, actionId); if(objectAction == null) { throw new RuntimeException(String.format("Unknown action '%s'", actionId)); } // TODO: background commands won't work for mixin actions... // ... we obtain the target from the bookmark service (above), which will // simply fail for a mixin. Instead we would need to serialize out the mixedInAdapter // and also capture the mixinType within the aim memento. final ObjectAdapter mixedInAdapter = null; final ObjectAdapter[] argAdapters = argAdaptersFor(aim); final ObjectAdapter resultAdapter = objectAction.execute( targetAdapter, mixedInAdapter, argAdapters, InteractionInitiatedBy.FRAMEWORK); if(resultAdapter != null) { Bookmark resultBookmark = CommandUtil.bookmarkFor(resultAdapter); backgroundCommand.setResult(resultBookmark); backgroundInteraction.getCurrentExecution().setReturned(resultAdapter.getObject()); } } else { final CommandDto dto = jaxbService.fromXml(CommandDto.class, memento); final MemberDto memberDto = dto.getMember(); final String memberId = memberDto.getMemberIdentifier(); final OidsDto oidsDto = CommandDtoUtils.targetsFor(dto); final List<OidDto> targetOidDtos = oidsDto.getOid(); final InteractionType interactionType = memberDto.getInteractionType(); if(interactionType == InteractionType.ACTION_INVOCATION) { final ActionDto actionDto = (ActionDto) memberDto; for (OidDto targetOidDto : targetOidDtos) { final ObjectAdapter targetAdapter = targetAdapterFor(targetOidDto); final ObjectAction objectAction = findObjectAction(targetAdapter, memberId); // we pass 'null' for the mixedInAdapter; if this action _is_ a mixin then // it will switch the targetAdapter to be the mixedInAdapter transparently final ObjectAdapter[] argAdapters = argAdaptersFor(actionDto); final ObjectAdapter resultAdapter = objectAction.execute( targetAdapter, null, argAdapters, InteractionInitiatedBy.FRAMEWORK); // // for the result adapter, we could alternatively have used... // (priorExecution populated by the push/pop within the interaction object) // // final Interaction.Execution priorExecution = backgroundInteraction.getPriorExecution(); // Object unused = priorExecution.getReturned(); // // REVIEW: this doesn't really make sense if >1 action // in any case, the capturing of the action interaction should be the // responsibility of auditing/profiling if(resultAdapter != null) { Bookmark resultBookmark = CommandUtil.bookmarkFor(resultAdapter); backgroundCommand.setResult(resultBookmark); } } } else { final PropertyDto propertyDto = (PropertyDto) memberDto; for (OidDto targetOidDto : targetOidDtos) { final Bookmark bookmark = Bookmark.from(targetOidDto); final Object targetObject = bookmarkService.lookup(bookmark); final ObjectAdapter targetAdapter = adapterFor(targetObject); final OneToOneAssociation property = findOneToOneAssociation(targetAdapter, memberId); final ObjectAdapter newValueAdapter = newValueAdapterFor(propertyDto); property.set(targetAdapter, newValueAdapter, InteractionInitiatedBy.FRAMEWORK); // there is no return value for property modifications. } } } } catch (RuntimeException e) { // hmmm, this doesn't really make sense if >1 action // // in any case, the capturing of the result of the action invocation should be the // responsibility of the interaction... backgroundCommand.setException(Throwables.getStackTraceAsString(e)); // lower down the stack the IsisTransactionManager will have set the transaction to abort // however, we don't want that to occur (because any changes made to the backgroundCommand itself // would also be rolled back, and it would keep getting picked up again by a scheduler for // processing); instead we clear the abort cause and ensure we can continue. transactionManager.getCurrentTransaction().clearAbortCauseAndContinue(); } // it's possible that there is no priorExecution, specifically if there was an exception // invoking the action. We therefore need to guard that case. final Interaction.Execution priorExecution = backgroundInteraction.getPriorExecution(); final Timestamp completedAt = priorExecution != null ? priorExecution.getCompletedAt() : clockService.nowAsJavaSqlTimestamp(); // close enough... backgroundCommand.setCompletedAt(completedAt); } private ObjectAction findObjectAction( final ObjectAdapter targetAdapter, final String actionId) throws RuntimeException { final ObjectSpecification specification = targetAdapter.getSpecification(); final ObjectAction objectAction = findActionElseNull(specification, actionId); if(objectAction == null) { throw new RuntimeException(String.format("Unknown action '%s'", actionId)); } return objectAction; } private OneToOneAssociation findOneToOneAssociation( final ObjectAdapter targetAdapter, final String propertyId) throws RuntimeException { final ObjectSpecification specification = targetAdapter.getSpecification(); final OneToOneAssociation property = findOneToOneAssociationElseNull(specification, propertyId); if(property == null) { throw new RuntimeException(String.format("Unknown property '%s'", propertyId)); } return property; } }); } protected ObjectAdapter newValueAdapterFor(final PropertyDto propertyDto) { final ValueWithTypeDto newValue = propertyDto.getNewValue(); final Object arg = CommonDtoUtils.getValue(newValue); return adapterFor(arg); } private static ObjectAction findActionElseNull( final ObjectSpecification specification, final String actionId) { final List<ObjectAction> objectActions = specification.getObjectActions(Contributed.INCLUDED); for (final ObjectAction objectAction : objectActions) { if(objectAction.getIdentifier().toClassAndNameIdentityString().equals(actionId)) { return objectAction; } } return null; } private static OneToOneAssociation findOneToOneAssociationElseNull( final ObjectSpecification specification, final String propertyId) { final List<ObjectAssociation> associations = specification.getAssociations(Contributed.INCLUDED); for (final ObjectAssociation association : associations) { if( association.getIdentifier().toClassAndNameIdentityString().equals(propertyId) && association instanceof OneToOneAssociation) { return (OneToOneAssociation) association; } } return null; } private ObjectAdapter[] argAdaptersFor(final ActionInvocationMemento aim) { final int numArgs = aim.getNumArgs(); final List<ObjectAdapter> argumentAdapters = Lists.newArrayList(); for(int i=0; i<numArgs; i++) { final ObjectAdapter argAdapter = argAdapterFor(aim, i); argumentAdapters.add(argAdapter); } return argumentAdapters.toArray(new ObjectAdapter[]{}); } private ObjectAdapter argAdapterFor(final ActionInvocationMemento aim, int num) { final Class<?> argType; try { argType = aim.getArgType(num); final Object arg = aim.getArg(num, argType); if(arg == null) { return null; } return argAdapterFor(argType, arg); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } } protected ObjectAdapter targetAdapterFor(final OidDto targetOidDto) { // // this is the original code, but it can be simplified ... // // (moved out to separate method so that, if proven wrong, can override as a patch) // final Bookmark bookmark = Bookmark.from(targetOidDto); // final Object targetObject = bookmarkService.lookup(bookmark); // final ObjectAdapter targetAdapter = adapterFor(targetObject); return adapterFor(targetOidDto); } protected ObjectAdapter argAdapterFor(final Class<?> argType, final Object arg) { // // this is the original code, but it can be simplified ... // // (moved out to separate method so that, if proven wrong, can override as a patch) // if(Bookmark.class != argType) { // return adapterFor(arg); // } else { // final Bookmark argBookmark = (Bookmark)arg; // final RootOid rootOid = RootOid.create(argBookmark); // return adapterFor(rootOid); // } return adapterFor(arg); } private ObjectAdapter[] argAdaptersFor(final ActionDto actionDto) { final List<ParamDto> params = paramDtosFrom(actionDto); final List<ObjectAdapter> args = Lists.newArrayList( Iterables.transform(params, new Function<ParamDto, ObjectAdapter>() { @Override public ObjectAdapter apply(final ParamDto paramDto) { final Object arg = CommonDtoUtils.getValue(paramDto); return adapterFor(arg); } }) ); return args.toArray(new ObjectAdapter[]{}); } private static List<ParamDto> paramDtosFrom(final ActionDto actionDto) { final ParamsDto parameters = actionDto.getParameters(); if (parameters != null) { final List<ParamDto> parameterList = parameters.getParameter(); if (parameterList != null) { return parameterList; } } return Collections.emptyList(); } // ////////////////////////////////////// @javax.inject.Inject private BookmarkService2 bookmarkService; @javax.inject.Inject private JaxbService jaxbService; @javax.inject.Inject private CommandContext commandContext; @javax.inject.Inject private InteractionContext interactionContext; @javax.inject.Inject private ClockService clockService; }