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 * — 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 — 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 — 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 — 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 — 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 * — 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 — 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 — 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 — 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}