001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.console.views;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.util.*;
022
023import org.apache.juneau.commons.bean.*;
024
025/**
026 * The bulk-mutate opt-in for a {@link ViewTable} — the second, INDEPENDENT half of the row-selection /
027 * bulk-mutate separability guarantee.
028 *
029 * <h5 class='section'>Two required parameters make it structurally, not conventionally, safe</h5>
030 * <p>
031 * There is no zero-argument (or permit-less) way to obtain a {@link BulkMutateDef}: {@link #create(WritePermit,
032 * SelectionDef)} takes both a {@link WritePermit} <b>and</b> the {@link SelectionDef} it operates against as
033 * required positional parameters. Omitting either is a <b>compile</b> failure, not a missed javadoc sentence
034 * &mdash; the same discipline the console's own {@code WriteGuard} uses for its {@code WritePermit}. This closes
035 * both halves of the opt-in separability guarantee by construction:
036 * <ul class='spaced-list'>
037 *    <li>The {@link WritePermit} proves the caller's own write-gate was consulted before this affordance is even
038 *       configured &mdash; a bulk-mutate control cannot be reached by a caller who forgot to check.
039 *    <li>The {@link SelectionDef} proves a stable-row-id selection mechanism already exists for this table to
040 *       target &mdash; a {@link BulkMutateDef} can never float free of the selection it acts on, so the reverse
041 *       question ("which rows does this bulk action see?") is never ambiguous. This is the only direction of
042 *       coupling between the two opt-ins: bulk-mutate requires selection, but selection never requires (or even
043 *       references) bulk-mutate &mdash; so enabling selection alone can never surface a bulk-mutate control.
044 * </ul>
045 *
046 * <h5 class='section'>Reuses {@link RowAction} and {@link ActionResult} verbatim &mdash; N per-row writes, per-target results</h5>
047 * <p>
048 * A bulk action is declared as an ordinary {@link RowAction} (the {@link RowAction} wire schema is completely
049 * untouched by this type); {@code juneau-views.js} executes a bulk action as N independent, per-row invocations
050 * of the SAME single-action submit path ({@code submitRowAction}) it already uses for the per-row action menu
051 * &mdash; one fail-closed CSRF POST per selected row, each with its OWN {@code data-juneau-inflight} marker (the
052 * per-row in-flight model, driven per target) and its OWN typed {@link ActionResult}, rendered
053 * independently into that row's own outcome banner. There is deliberately <b>no</b> aggregate "bulk result": a
054 * partial failure among N targets can never be hidden behind an aggregate success, and each target's
055 * {@code data-juneau-inflight} marker clears on ITS OWN terminal outcome (success/failure/refusal/unknown) so a
056 * stuck marker on one target can never halt the whole table's polling.
057 *
058 * <h5 class='section'>Its own, independent contract version</h5>
059 * <p>
060 * Unlike {@link SelectionDef} (which is pure DOM attributes, never wire data), a bulk action LIST is itself wire
061 * data the client must parse &mdash; so it travels as its own JSON sidecar (distinct from the {@code VIEW_META}
062 * one), with its own {@link #CONTRACT_VERSION}, exactly mirroring how {@link ActionResult} versions itself
063 * independently of {@code VIEW_META} rather than aliasing it. A version bump here can therefore
064 * NEVER force a {@code VIEW_META} bump (or vice versa): the two contracts are unrelated, so this
065 * opt-in cannot back the framework into the very lockstep the opt-in separability guard exists to avoid. The
066 * {@link WritePermit} carried by this builder is a server-side-only construction guard and is <b>never</b>
067 * serialized &mdash; only {@link #contractVersion} and {@link #actions} reach the wire.
068 *
069 * <h5 class='section'>Example:</h5>
070 * <p class='bjava'>
071 *    SelectionDef <jv>selection</jv> = SelectionDef.<jsm>create</jsm>(<js>"id"</js>);
072 *    <jc>// Requires a permit the CALLER's own write-gate has already minted, and the selection it targets.</jc>
073 *    BulkMutateDef <jv>bulk</jv> = BulkMutateDef.<jsm>create</jsm>(WritePermit.<jsm>forCapability</jsm>(<js>"incident:bulk-ack"</js>), <jv>selection</jv>)
074 *       .actions(RowAction.<jsm>create</jsm>(<js>"ack"</js>).label(<js>"Acknowledge"</js>)
075 *          .endpoint(<js>"servlet:/incidents/ack"</js>).method(RowAction.Method.<jsf>POST</jsf>));
076 * </p>
077 *
078 * <h5 class='section'>See Also:</h5>
079 * <ul>
080 *    <li class='jc'>{@link SelectionDef}
081 *    <li class='jc'>{@link WritePermit}
082 *    <li class='jc'>{@link RowAction}
083 *    <li class='jc'>{@link ActionResult}
084 * </ul>
085 *
086 * @since 10.0.0
087 */
088@BeanType(properties="contractVersion,actions")
089public final class BulkMutateDef {
090
091   /**
092    * The frozen bulk-actions contract version &mdash; its <b>own</b> discriminator, deliberately NOT aliased to
093    * {@code VIEW_META} (see the class javadoc). Bumped only on a breaking wire change to this
094    * contract.
095    */
096   public static final String CONTRACT_VERSION = "2";
097
098   /** The frozen contract-version discriminator (always {@value #CONTRACT_VERSION} for this contract). */
099   public String contractVersion = CONTRACT_VERSION;
100
101   /** The declared bulk actions, in menu order. Each is an ordinary, unmodified {@link RowAction}. */
102   public List<RowAction> actions;
103
104   /**
105    * The construction-time proof that the caller's own write-gate was consulted. Deliberately private and NOT
106    * listed in {@code @BeanType(properties=...)}: it must never reach the wire.
107    */
108   private final WritePermit permit;
109
110   /** The selection this bulk config operates against. Also excluded from the wire (see {@link #permit}). */
111   private final SelectionDef selection;
112
113   private BulkMutateDef(WritePermit permit, SelectionDef selection) {
114      this.permit = permit;
115      this.selection = selection;
116   }
117
118   /**
119    * Starts a new {@link BulkMutateDef}, requiring proof that the caller's write-gate was consulted and the
120    * selection this bulk config will operate against.
121    *
122    * @param permit A permit the caller's OWN write-gate minted after its own checks passed. Must not be
123    *    <jk>null</jk>.
124    * @param selection The {@link SelectionDef} this bulk config operates against. Must not be <jk>null</jk>.
125    * @return A new {@link BulkMutateDef} with no actions yet declared.
126    * @throws IllegalArgumentException If {@code permit} or {@code selection} is <jk>null</jk>.
127    */
128   public static BulkMutateDef create(WritePermit permit, SelectionDef selection) {
129      if (permit == null)
130         throw iaex("BulkMutateDef requires a non-null WritePermit (the caller's own write-gate must mint one first).");
131      if (selection == null)
132         throw iaex("BulkMutateDef requires a non-null SelectionDef (the selection this bulk config operates against).");
133      return new BulkMutateDef(permit, selection);
134   }
135
136   /**
137    * Declares the bulk actions.
138    *
139    * @param value The actions, in menu order. Each declares a mutating request executed once per selected row
140    *    (see the class javadoc's N-per-row-writes section). Must not be <jk>null</jk> or empty.
141    * @return This object.
142    * @throws IllegalArgumentException If {@code value} is <jk>null</jk> or empty.
143    */
144   @SuppressWarnings({
145      "java:S1845" // Fluent-builder setter intentionally mirrors the wire field name (Juneau DSL convention); part of the public fluent API.
146   })
147   public BulkMutateDef actions(RowAction...value) {
148      if (value == null || value.length == 0)
149         throw iaex("BulkMutateDef requires at least one action.");
150      for (var a : value)
151         if (RowAction.BulkMode.AGGREGATE.wire().equals(a.bulkMode) && a.endpoint != null && a.endpoint.contains("{"))
152            throw iaex("Bulk action '%s' has mode 'aggregate' but its endpoint '%s' contains a {field} token; aggregate endpoints receive {ids} in the body.", a.id, a.endpoint);
153      actions = l(value);
154      return this;
155   }
156
157   /**
158    * The permit proving the caller's write-gate was consulted before this config was built.
159    *
160    * @return The permit. Never <jk>null</jk>.
161    */
162   public WritePermit permit() {
163      return permit;
164   }
165
166   /**
167    * The selection this bulk config operates against.
168    *
169    * @return The selection. Never <jk>null</jk>.
170    */
171   public SelectionDef selection() {
172      return selection;
173   }
174}