/* * 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.specloader.specimpl; import java.util.Collections; import java.util.List; import java.util.concurrent.Callable; import com.google.common.base.Objects; import com.google.common.collect.Lists; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.isis.applib.ApplicationException; import org.apache.isis.applib.RecoverableException; import org.apache.isis.applib.annotation.ActionSemantics; import org.apache.isis.applib.annotation.Bulk; import org.apache.isis.applib.annotation.InvokedOn; import org.apache.isis.applib.annotation.Where; import org.apache.isis.applib.filter.Filter; import org.apache.isis.applib.services.command.Command; import org.apache.isis.core.commons.exceptions.UnknownTypeException; import org.apache.isis.core.metamodel.adapter.ObjectAdapter; import org.apache.isis.core.metamodel.consent.Consent; import org.apache.isis.core.metamodel.consent.InteractionInitiatedBy; import org.apache.isis.core.metamodel.consent.InteractionResultSet; import org.apache.isis.core.metamodel.facetapi.Facet; import org.apache.isis.core.metamodel.facetapi.FacetHolder; import org.apache.isis.core.metamodel.facetapi.FeatureType; import org.apache.isis.core.metamodel.facets.FacetedMethod; import org.apache.isis.core.metamodel.facets.FacetedMethodParameter; import org.apache.isis.core.metamodel.facets.actions.action.invocation.ActionInvocationFacet; import org.apache.isis.core.metamodel.facets.actions.action.invocation.CommandUtil; import org.apache.isis.core.metamodel.facets.actions.bulk.BulkFacet; import org.apache.isis.core.metamodel.facets.actions.defaults.ActionDefaultsFacet; import org.apache.isis.core.metamodel.facets.actions.prototype.PrototypeFacet; import org.apache.isis.core.metamodel.facets.actions.semantics.ActionSemanticsFacet; import org.apache.isis.core.metamodel.facets.param.choices.ActionChoicesFacet; import org.apache.isis.core.metamodel.facets.param.choices.ActionParameterChoicesFacet; import org.apache.isis.core.metamodel.facets.param.defaults.ActionParameterDefaultsFacet; import org.apache.isis.core.metamodel.interactions.ActionUsabilityContext; import org.apache.isis.core.metamodel.interactions.ActionValidityContext; import org.apache.isis.core.metamodel.interactions.ActionVisibilityContext; import org.apache.isis.core.metamodel.interactions.InteractionUtils; import org.apache.isis.core.metamodel.interactions.UsabilityContext; import org.apache.isis.core.metamodel.interactions.ValidityContext; import org.apache.isis.core.metamodel.interactions.VisibilityContext; import org.apache.isis.core.metamodel.services.ServicesInjector; import org.apache.isis.core.metamodel.services.command.CommandDtoServiceInternal; import org.apache.isis.core.metamodel.spec.ActionType; import org.apache.isis.core.metamodel.spec.DomainModelException; import org.apache.isis.core.metamodel.spec.ObjectSpecification; import org.apache.isis.core.metamodel.spec.feature.ObjectAction; import org.apache.isis.core.metamodel.spec.feature.ObjectActionParameter; import org.apache.isis.schema.cmd.v1.CommandDto; public class ObjectActionDefault extends ObjectMemberAbstract implements ObjectAction { private final static Logger LOG = LoggerFactory.getLogger(ObjectActionDefault.class); public static ActionType getType(final String typeStr) { final ActionType type = ActionType.valueOf(typeStr); if (type == null) { throw new IllegalArgumentException(); } return type; } //region > fields /** * Lazily initialized by {@link #getParameters()} (so don't use directly!) */ List<ObjectActionParameter> parameters; //endregion //region > constructors public ObjectActionDefault( final FacetedMethod facetedMethod, final ServicesInjector servicesInjector) { super(facetedMethod, FeatureType.ACTION, servicesInjector); } //endregion //region > ReturnType, OnType, Actions (set) /** * Always returns <tt>null</tt>. */ @Override public ObjectSpecification getSpecification() { return null; } @Override public ObjectSpecification getReturnType() { final ActionInvocationFacet facet = getActionInvocationFacet(); return facet.getReturnType(); } /** * Returns true if the represented action returns something, else returns * false. */ @Override public boolean hasReturn() { if(getReturnType() == null) { // this shouldn't happen; return Type always defined, even if represents void.class return false; } return getReturnType() != getSpecificationLoader().loadSpecification(void.class); } @Override public ObjectSpecification getOnType() { final ActionInvocationFacet facet = getActionInvocationFacet(); return facet.getOnType(); } @Override public ActionSemantics.Of getSemantics() { final ActionSemanticsFacet facet = getFacet(ActionSemanticsFacet.class); return facet != null? facet.value(): ActionSemantics.Of.NON_IDEMPOTENT; } //endregion //region > Type @Override public ActionType getType() { return getType(this); } private static ActionType getType(final FacetHolder facetHolder) { Facet facet = facetHolder.getFacet(PrototypeFacet.class); if (facet != null) { return ActionType.PROTOTYPE; } return ActionType.USER; } //endregion //region > Parameters @Override public int getParameterCount() { return getFacetedMethod().getParameters().size(); } /** * Build lazily by {@link #getParameters()}. * * <p> * Although this is lazily loaded, the method is also <tt>synchronized</tt> * so there shouldn't be any thread race conditions. */ @Override public List<ObjectActionParameter> getParameters() { if (parameters == null) { parameters = determineParameters(); } return parameters; } protected synchronized List<ObjectActionParameter> determineParameters() { if (parameters != null) { // because possible race condition (caller isn't synchronized) return parameters; } final int parameterCount = getParameterCount(); final List<FacetedMethodParameter> paramPeers = getFacetedMethod().getParameters(); final List<ObjectActionParameter> parameters = Lists.newArrayList(); for (int paramNum = 0; paramNum < parameterCount; paramNum++) { final FacetedMethodParameter paramPeer = paramPeers.get(paramNum); final ObjectSpecification specification = ObjectMemberAbstract .getSpecification(getSpecificationLoader(), paramPeer.getType()); // previously we threw an exception here if the specification represented a collection. No longer! final ObjectActionParameter parameter = paramPeer.getFeatureType() == FeatureType.ACTION_PARAMETER_SCALAR ? new OneToOneActionParameterDefault(paramNum, this, paramPeer) : new OneToManyActionParameterDefault(paramNum, this, paramPeer); parameters.add(parameter); } return parameters; } @Override public List<ObjectSpecification> getParameterTypes() { final List<ObjectSpecification> parameterTypes = Lists.newArrayList(); final List<ObjectActionParameter> parameters = getParameters(); for (final ObjectActionParameter parameter : parameters) { parameterTypes.add(parameter.getSpecification()); } return parameterTypes; } @Override public ObjectActionParameter getParameterById(final String paramId) { final List<ObjectActionParameter> allParameters = getParameters(); for (int i = 0; i < allParameters.size(); i++) { final ObjectActionParameter param = allParameters.get(i); if (Objects.equal(paramId, param.getId())) { return param; } } return null; } @Override public ObjectActionParameter getParameterByName(final String paramName) { final List<ObjectActionParameter> allParameters = getParameters(); for (int i = 0; i < allParameters.size(); i++) { final ObjectActionParameter param = allParameters.get(i); if (Objects.equal(paramName, param.getName())) { return param; } } return null; } @Override public List<ObjectActionParameter> getParameters(final Filter<ObjectActionParameter> filter) { final List<ObjectActionParameter> allParameters = getParameters(); final List<ObjectActionParameter> selectedParameters = Lists.newArrayList(); for (int i = 0; i < allParameters.size(); i++) { if (filter.accept(allParameters.get(i))) { selectedParameters.add(allParameters.get(i)); } } return selectedParameters; } ObjectActionParameter getParameter(final int position) { final List<ObjectActionParameter> parameters = getParameters(); if (position >= parameters.size()) { throw new IllegalArgumentException("getParameter(int): only " + parameters.size() + " parameters, position=" + position); } return parameters.get(position); } //endregion //region > visable, usable @Override public VisibilityContext<?> createVisibleInteractionContext( final ObjectAdapter targetObjectAdapter, final InteractionInitiatedBy interactionInitiatedBy, Where where) { return new ActionVisibilityContext(targetObjectAdapter, this, getIdentifier(), interactionInitiatedBy, where); } @Override public UsabilityContext<?> createUsableInteractionContext( final ObjectAdapter targetObjectAdapter, final InteractionInitiatedBy interactionInitiatedBy, Where where) { return new ActionUsabilityContext(targetObjectAdapter, this, getIdentifier(), interactionInitiatedBy, where); } //endregion //region > validate @Override public Consent isProposedArgumentSetValid( final ObjectAdapter targetObject, final ObjectAdapter[] proposedArguments, final InteractionInitiatedBy interactionInitiatedBy) { final InteractionResultSet resultSet = new InteractionResultSet(); validateArgumentsIndividually(targetObject, proposedArguments, interactionInitiatedBy, resultSet); if (resultSet.isAllowed()) { // only check the action's own validity if all the arguments are OK. validateArgumentSet(targetObject, proposedArguments, interactionInitiatedBy, resultSet); } return resultSet.createConsent(); } protected void validateArgumentsIndividually( final ObjectAdapter objectAdapter, final ObjectAdapter[] proposedArguments, final InteractionInitiatedBy interactionInitiatedBy, final InteractionResultSet resultSet) { final List<ObjectActionParameter> actionParameters = getParameters(); if (proposedArguments != null) { for (int i = 0; i < proposedArguments.length; i++) { final ValidityContext<?> ic = actionParameters.get(i).createProposedArgumentInteractionContext( objectAdapter, proposedArguments, i, interactionInitiatedBy ); InteractionUtils.isValidResultSet(getParameter(i), ic, resultSet); } } } protected void validateArgumentSet( final ObjectAdapter objectAdapter, final ObjectAdapter[] proposedArguments, final InteractionInitiatedBy interactionInitiatedBy, final InteractionResultSet resultSet) { final ValidityContext<?> ic = createActionInvocationInteractionContext( objectAdapter, proposedArguments, interactionInitiatedBy); InteractionUtils.isValidResultSet(this, ic, resultSet); } ActionValidityContext createActionInvocationInteractionContext( final ObjectAdapter targetObject, final ObjectAdapter[] proposedArguments, final InteractionInitiatedBy interactionInitiatedBy) { return new ActionValidityContext(targetObject, this, getIdentifier(), proposedArguments, interactionInitiatedBy); } //endregion //region > executeWithRuleChecking, execute @Override public ObjectAdapter executeWithRuleChecking( final ObjectAdapter target, final ObjectAdapter mixedInAdapter, final ObjectAdapter[] arguments, final InteractionInitiatedBy interactionInitiatedBy, final Where where) { // see it? final Consent visibility = isVisible(target, interactionInitiatedBy, where); if (visibility.isVetoed()) { throw new AuthorizationException(); } // use it? final Consent usability = isUsable(target, interactionInitiatedBy, where); if(usability.isVetoed()) { throw new AuthorizationException(); } // do it? final Consent validity = isProposedArgumentSetValid(target, arguments, interactionInitiatedBy); if(validity.isVetoed()) { throw new RecoverableException(validity.getReason()); } return execute(target, mixedInAdapter, arguments, interactionInitiatedBy); } /** * Sets up the {@link Command}, then delegates off to * {@link #executeInternal(ObjectAdapter, ObjectAdapter, ObjectAdapter[], InteractionInitiatedBy) executeInternal} * to invoke the {@link ActionInvocationFacet invocation facet}. * * @param mixedInAdapter - will be null for regular actions, and for mixin actions. When a mixin action invokes its underlying mixedIn action, then will be populated (so that the ActionDomainEvent can correctly provide the underlying mixin) */ @Override public ObjectAdapter execute( final ObjectAdapter targetAdapter, final ObjectAdapter mixedInAdapter, final ObjectAdapter[] argumentAdapters, final InteractionInitiatedBy interactionInitiatedBy) { setupCommand(targetAdapter, argumentAdapters); return this.executeInternal(targetAdapter, mixedInAdapter, argumentAdapters, interactionInitiatedBy); } /** * private API, called by mixins and contributees. */ public ObjectAdapter executeInternal( final ObjectAdapter targetAdapter, final ObjectAdapter mixedInAdapter, final ObjectAdapter[] argumentAdapters, final InteractionInitiatedBy interactionInitiatedBy) { final ActionInvocationFacet facet = getFacet(ActionInvocationFacet.class); return facet.invoke(this, targetAdapter, mixedInAdapter, argumentAdapters, interactionInitiatedBy); } protected ActionInvocationFacet getActionInvocationFacet() { return getFacetedMethod().getFacet(ActionInvocationFacet.class); } //endregion //region > defaults @Override public ObjectAdapter[] getDefaults(final ObjectAdapter target) { final int parameterCount = getParameterCount(); final List<ObjectActionParameter> parameters = getParameters(); final Object[] parameterDefaultPojos; final ActionDefaultsFacet facet = getFacet(ActionDefaultsFacet.class); if (!facet.isNoop()) { // use the old defaultXxx approach parameterDefaultPojos = facet.getDefaults(target); if (parameterDefaultPojos.length != parameterCount) { throw new DomainModelException("Defaults array of incompatible size; expected " + parameterCount + " elements, but was " + parameterDefaultPojos.length + " for " + facet); } for (int i = 0; i < parameterCount; i++) { if (parameterDefaultPojos[i] != null) { final ObjectSpecification componentSpec = getSpecificationLoader().loadSpecification(parameterDefaultPojos[i].getClass()); final ObjectSpecification parameterSpec = parameters.get(i).getSpecification(); if (!componentSpec.isOfType(parameterSpec)) { throw new DomainModelException("Defaults type incompatible with parameter " + (i + 1) + " type; expected " + parameterSpec.getFullIdentifier() + ", but was " + componentSpec.getFullIdentifier()); } } } } else { // use the new defaultNXxx approach for each param in turn // (the reflector will have made sure both aren't installed). parameterDefaultPojos = new Object[parameterCount]; for (int i = 0; i < parameterCount; i++) { final ActionParameterDefaultsFacet paramFacet = parameters.get(i).getFacet(ActionParameterDefaultsFacet.class); if (paramFacet != null && !paramFacet.isNoop()) { parameterDefaultPojos[i] = paramFacet.getDefault(target, null); } else { parameterDefaultPojos[i] = null; } } } final ObjectAdapter[] parameterDefaultAdapters = new ObjectAdapter[parameterCount]; for (int i = 0; i < parameterCount; i++) { parameterDefaultAdapters[i] = adapterFor(parameterDefaultPojos[i]); } return parameterDefaultAdapters; } private ObjectAdapter adapterFor(final Object pojo) { return pojo == null ? null : getPersistenceSessionService().adapterFor(pojo); } private static ThreadLocal<List<ObjectAdapter>> commandTargetAdaptersHolder = new ThreadLocal<>(); /** * A horrible hack to be able to persist a number of adapters in the command object. * * <p> * What is really needed is to be able to invoke an action on a number of adapters all together. * </p> */ public static <T> T withTargetAdapters(final List<ObjectAdapter> adapters, final Callable<T> callable) { commandTargetAdaptersHolder.set(adapters); try { return callable.call(); } catch (Exception e) { throw new ApplicationException(e); } finally { commandTargetAdaptersHolder.set(null); } } //endregion //region > choices @Override public ObjectAdapter[][] getChoices( final ObjectAdapter target, final InteractionInitiatedBy interactionInitiatedBy) { final int parameterCount = getParameterCount(); Object[][] parameterChoicesPojos; final ActionChoicesFacet facet = getFacet(ActionChoicesFacet.class); final List<ObjectActionParameter> parameters = getParameters(); if (!facet.isNoop()) { // using the old choicesXxx() approach parameterChoicesPojos = facet.getChoices(target, interactionInitiatedBy); // if no options, or not the right number of pojos, then default if (parameterChoicesPojos == null) { parameterChoicesPojos = new Object[parameterCount][]; } else if (parameterChoicesPojos.length != parameterCount) { throw new DomainModelException( String.format("Choices array of incompatible size; expected %d elements, but was %d for %s", parameterCount, parameterChoicesPojos.length, facet)); } } else { // use the new choicesNXxx approach for each param in turn // (the reflector will have made sure both aren't installed). parameterChoicesPojos = new Object[parameterCount][]; for (int i = 0; i < parameterCount; i++) { final ActionParameterChoicesFacet paramFacet = parameters.get(i).getFacet(ActionParameterChoicesFacet.class); if (paramFacet != null && !paramFacet.isNoop()) { parameterChoicesPojos[i] = paramFacet.getChoices(target, null, interactionInitiatedBy); } else { parameterChoicesPojos[i] = new Object[0]; } } } final ObjectAdapter[][] parameterChoicesAdapters = new ObjectAdapter[parameterCount][]; for (int i = 0; i < parameterCount; i++) { final ObjectSpecification paramSpec = parameters.get(i).getSpecification(); if (parameterChoicesPojos[i] != null && parameterChoicesPojos[i].length > 0) { ObjectActionParameterAbstract.checkChoicesOrAutoCompleteType( getSpecificationLoader(), parameterChoicesPojos[i], paramSpec); parameterChoicesAdapters[i] = new ObjectAdapter[parameterChoicesPojos[i].length]; for (int j = 0; j < parameterChoicesPojos[i].length; j++) { parameterChoicesAdapters[i][j] = adapterFor(parameterChoicesPojos[i][j]); } } else if (paramSpec.isNotCollection()) { parameterChoicesAdapters[i] = new ObjectAdapter[0]; } else { throw new UnknownTypeException(paramSpec); } if (parameterChoicesAdapters[i].length == 0) { parameterChoicesAdapters[i] = null; } } return parameterChoicesAdapters; } /** * Internal API */ @Override public void setupBulkActionInvocationContext(final ObjectAdapter targetAdapter) { final Object targetPojo = ObjectAdapter.Util.unwrap(targetAdapter); final BulkFacet bulkFacet = getFacetHolder().getFacet(BulkFacet.class); if (bulkFacet != null) { final org.apache.isis.applib.services.actinvoc.ActionInvocationContext actionInvocationContext = getActionInvocationContext(); if (actionInvocationContext != null && actionInvocationContext.getInvokedOn() == null) { actionInvocationContext.setInvokedOn(InvokedOn.OBJECT); actionInvocationContext.setDomainObjects(Collections.singletonList(targetPojo)); } final Bulk.InteractionContext bulkInteractionContext = getBulkInteractionContext(); if (bulkInteractionContext != null && bulkInteractionContext.getInvokedAs() == null) { bulkInteractionContext.setInvokedAs(Bulk.InteractionContext.InvokedAs.REGULAR); bulkInteractionContext.setDomainObjects(Collections.singletonList(targetPojo)); } } } @Override public boolean isPrototype() { return getType().isPrototype(); } /** * Internal API, called by the various implementations of {@link ObjectAction} ({@link ObjectActionDefault default}, * {@link ObjectActionMixedIn mixed-in} and {@link ObjectActionContributee contributee}). */ public void setupCommand( final ObjectAdapter targetAdapter, final ObjectAdapter[] argumentAdapters) { setupCommandTarget(targetAdapter, argumentAdapters); setupCommandMemberIdentifier(); setupCommandMementoAndExecutionContext(targetAdapter, argumentAdapters); } private void setupCommandTarget( final ObjectAdapter targetAdapter, final ObjectAdapter[] argumentAdapters) { final String arguments = CommandUtil.argDescriptionFor(this, argumentAdapters); setupCommandTarget(targetAdapter, arguments); } private void setupCommandMementoAndExecutionContext( final ObjectAdapter targetAdapter, final ObjectAdapter[] argumentAdapters) { final CommandDtoServiceInternal commandDtoServiceInternal = getCommandDtoService(); final List<ObjectAdapter> commandTargetAdapters = commandTargetAdaptersHolder.get() != null ? commandTargetAdaptersHolder.get() : Collections.singletonList(targetAdapter); final CommandDto dto = commandDtoServiceInternal.asCommandDto( commandTargetAdapters, this, argumentAdapters); setupCommandDtoAndExecutionContext(dto); } //endregion //region > toString @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append("Action ["); sb.append(super.toString()); sb.append(",type="); sb.append(getType()); sb.append(",returns="); sb.append(getReturnType()); sb.append(",parameters={"); for (int i = 0; i < getParameterCount(); i++) { if (i > 0) { sb.append(","); } sb.append(getParameters().get(i).getSpecification().getShortIdentifier()); } sb.append("}]"); return sb.toString(); } //endregion //region > services (lookup) protected Bulk.InteractionContext getBulkInteractionContext() { return lookupService(Bulk.InteractionContext.class); } protected org.apache.isis.applib.services.actinvoc.ActionInvocationContext getActionInvocationContext() { return lookupService(org.apache.isis.applib.services.actinvoc.ActionInvocationContext.class); } //endregion }