in core/metamodel/src/main/java/org/apache/causeway/core/metamodel/interactions/managed/ActionInteraction.java [39:268]
public record ActionInteraction(
String memberId,
/**
* optionally the action's metamodel, based on whether was found by actionId
*/
@NonNull Optional<ObjectAction> objectAction,
@NonNull InteractionRailway<ManagedAction> railway)
implements MemberInteraction<ManagedAction, ActionInteraction> {
public enum SemanticConstraint {
NONE,
IDEMPOTENT,
SAFE
}
public record Result(
ManagedAction managedAction,
Can<ManagedObject> parameterList,
ManagedObject actionReturnedObject) {
}
public static interface ParameterInvalidCallback {
void onParameterInvalid(ManagedParameter managedParameter, InteractionVeto veto);
}
// -- FACTORIES
public static ActionInteraction start(
final @NonNull ManagedObject owner,
final @NonNull String memberId,
final @NonNull Where where) {
return startWithMultiselect(owner, memberId, where, Can::empty);
}
public static ActionInteraction startWithMultiselect(
final @NonNull ManagedObject owner,
final @NonNull String actionId,
final @NonNull Where where,
final @NonNull MultiselectChoices multiselectChoices) {
var managedAction = ManagedAction.lookupActionWithMultiselect(owner, actionId, where, multiselectChoices);
final InteractionRailway<ManagedAction> railway = managedAction.isPresent()
? InteractionRailway.success(managedAction.get())
: InteractionRailway.veto(InteractionVeto.notFound(Identifier.Type.ACTION, actionId));
return new ActionInteraction(
actionId,
managedAction.map(x->x.getAction()),
railway);
}
/** Supports composite-value-types via mixin (in case detected). */
public static ActionInteraction startAsBoundToProperty(
final ManagedProperty associatedWithProperty,
final String memberId,
final Where where) {
var propertyOwner = associatedWithProperty.getOwner();
var prop = associatedWithProperty.getMetaModel();
var elementType = prop.getElementType();
var valueFacet = elementType.isValue()
? (ValueFacet<?>) elementType.valueFacet().orElse(null)
: null;
if(valueFacet!=null
&& valueFacet.isCompositeValueType()
//XXX guard against memberId collision,
// such that if there is a conflict, the conventional member wins
// (maybe improve programming model so this cannot happen)
&& propertyOwner.objSpec().getAction(memberId, MixedIn.INCLUDED).isEmpty()) {
var compositeValueNullable = prop.get(propertyOwner);
var compositeValue =
ManagedObjects.nullOrEmptyToDefault(elementType, compositeValueNullable, ()->
valueFacet.selectDefaultsProviderForAttribute(prop)
.orElseThrow(()->onMissingDefaultsProvider(prop))
.getDefaultValue());
var mixinAction = valueFacet.selectCompositeValueMixinForProperty(associatedWithProperty);
if(mixinAction.isPresent()) {
var managedAction = ManagedAction.of(compositeValue, mixinAction.get(), where);
return ActionInteraction.wrap(managedAction);
}
}
// fallback if not a composite value
return ActionInteraction.start(propertyOwner, memberId, where);
}
/** Supports composite-value-types via mixin (in case detected). */
public static ActionInteraction startAsBoundToParameter(
final ParameterNegotiationModel parameterNegotiationModel,
final int paramIndex,
final String memberId,
final Where where) {
var actionOwner = parameterNegotiationModel.getActionTarget();
var param = parameterNegotiationModel.getParamModels().getElseFail(paramIndex);
var elementType = param.getMetaModel().getElementType();
var valueFacet = elementType.isValue()
? (ValueFacet<?>) elementType.valueFacet().orElse(null)
: null;
if(valueFacet!=null
&& valueFacet.isCompositeValueType()
//XXX guard against memberId collision,
// such that if there is a conflict, the conventional member wins
// (maybe improve programming model so this cannot happen)
&& actionOwner.objSpec().getAction(memberId, MixedIn.INCLUDED).isEmpty()) {
var compositeValueNullable = parameterNegotiationModel.getParamValue(paramIndex);
var compositeValue =
ManagedObjects.nullOrEmptyToDefault(elementType, compositeValueNullable, ()->
valueFacet.selectDefaultsProviderForAttribute(param.getMetaModel())
.orElseThrow(()->onMissingDefaultsProvider(param.getMetaModel()))
.getDefaultValue());
var mixinAction = valueFacet.selectCompositeValueMixinForParameter(parameterNegotiationModel, paramIndex);
if(mixinAction.isPresent()) {
var managedAction = ManagedAction.of(compositeValue, mixinAction.get(), where);
return ActionInteraction.wrap(managedAction);
}
}
// else if not a composite value
return ActionInteraction.start(actionOwner, memberId, where);
}
public static ActionInteraction wrap(final @NonNull ManagedAction managedAction) {
var action = managedAction.getAction();
return new ActionInteraction(
action.getId(),
Optional.of(action),
InteractionRailway.success(managedAction));
}
public static ActionInteraction empty(final String actionId) {
return new ActionInteraction(
actionId,
Optional.empty(),
InteractionRailway.veto(InteractionVeto.notFound(Identifier.Type.ACTION, actionId)));
}
// -- METHODS
public ObjectAction getObjectActionElseFail() {
return objectAction.orElseThrow(()->_Exceptions
.noSuchElement("could not resolve action by memberId '%s'", memberId));
}
public Optional<Identifier> getFeatureIdentifier() {
return objectAction.map(ObjectAction::getFeatureIdentifier);
}
public ActionInteraction checkSemanticConstraint(final @NonNull SemanticConstraint semanticConstraint) {
railway.update(action -> switch(semanticConstraint) {
case NONE -> Optional.empty();
case IDEMPOTENT -> action.getAction().getSemantics().isIdempotentInNature()
? Optional.empty()
: Optional.of(InteractionVeto.actionNotIdempotent(action));
case SAFE -> action.getAction().getSemantics().isSafeInNature()
? Optional.empty()
: Optional.of(InteractionVeto.actionNotSafe(action));
}
);
return this;
}
public Optional<ParameterNegotiationModel> startParameterNegotiation() {
return getManagedAction()
.map(ManagedAction::startParameterNegotiation);
}
public Railway<InteractionVeto, ManagedObject> invokeWith(final ParameterNegotiationModel pendingArgs) {
pendingArgs.activateValidationFeedback();
var veto = validate(pendingArgs);
if(veto.isPresent()) return Railway.failure(veto.get());
var action = railway.getSuccessElseFail();
var actionResultOrVeto = action.invoke(pendingArgs.getParamValues());
return actionResultOrVeto;
}
public ManagedObject invokeWithRuleChecking(
final ParameterNegotiationModel pendingArgs) throws AuthorizationException {
var action = railway.getSuccessElseFail();
return action.invokeWithRuleChecking(pendingArgs.getParamValues());
}
public Optional<InteractionVeto> validate(
final @NonNull ParameterNegotiationModel pendingArgs) {
if(railway.isVeto()) return railway.getVeto();
var validityConsent = pendingArgs.validateParameterSet(); // full validation
return validityConsent!=null
&& validityConsent.isVetoed()
? Optional.of(InteractionVeto.actionParamInvalid(validityConsent))
: Optional.empty();
}
/**
* @return optionally the ManagedAction based on whether there
* was no interaction veto within the originating chain
*/
public Optional<ManagedAction> getManagedAction() {
return railway.getSuccess();
}
/**
* @return this Interaction's ManagedAction
* @throws X if there was any interaction veto within the originating chain
*/
public <X extends Throwable> ManagedAction getManagedActionElseThrow(
final Function<InteractionVeto, ? extends X> onFailure) throws X {
return getManagedMemberElseThrow(onFailure);
}
public <X extends Throwable> ManagedAction getManagedActionElseFail() {
return getManagedActionElseThrow(veto->
_Exceptions.unrecoverable("action vetoed: " + veto.getReason()));
}
// -- HELPER
private static RuntimeException onMissingDefaultsProvider(final ObjectFeature feature) {
return _Exceptions.unrecoverable("Could not find a DefaultsProvider for ObjectFeature %s",
feature.getFeatureIdentifier());
}
}