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 org.apache.juneau.commons.bean.*; 022 023/** 024 * The typed result a row action's submit returns — the load-bearing half of the write path. 025 * 026 * <p> 027 * A raw HTML fragment response cannot carry the distinction the consumer's non-optimistic UI rule requires: a row 028 * must not show "acknowledged" because a request was <i>sent</i>, only because the remote system was read back and 029 * said so. This typed result carries exactly that distinction — an {@link Outcome outcome discriminator}, the 030 * authoritative {@link #row} data to re-render from, a {@link #replay} marker, and a {@link #refusalCode} — so 031 * the {@code juneau-views.js} runtime can paint what actually happened (success, failure, a <i>named</i> refusal, or 032 * an honest unknown) rather than what the request asked for. 033 * 034 * <h5 class='section'>Its own, independent contract version</h5> 035 * <p> 036 * The action-result is a <b>third</b> independently-versioned wire contract, alongside {@code VIEW_META} 037 * and the FTL card/slot envelope. Its 038 * {@link #CONTRACT_VERSION} is deliberately its <b>own</b> constant and is <b>not</b> aliased to 039 * {@code VIEW_META}: aliasing a per-view contract's version onto a separate contract couples two 040 * unrelated client/server locksteps, so this contract versions itself. 041 * 042 * <h5 class='section'>Refusals are an opaque, namespaced code — not a framework-closed enum</h5> 043 * <p> 044 * A refusal's {@link #refusalCode} is an <b>opaque namespaced string</b>, not a framework-closed enum and not a 045 * free-text message. Refusals span two independent sources, and the framework must be able to render a refusal it 046 * has never heard of: 047 * <ul class='spaced-list'> 048 * <li><b>Framework transport codes</b> come from 049 * {@link org.apache.juneau.rest.server.filter.BoundaryReason#name() BoundaryReason.name()} 050 * (e.g. {@code CSRF_TOKEN_MISSING}), already on the wire as the {@code X-Request-Boundary} header and the 051 * rejection envelope's {@code reason}. 052 * <li><b>Consumer codes</b> are namespaced {@code app:} / {@code write-guard:} strings for concepts Juneau knows 053 * nothing about ("not armed", "arming expired", "confirmation does not match this incident", "a previous 054 * attempt's outcome is unknown"). 055 * </ul> 056 * <p> 057 * The runtime renders ANY code as a visible <i>named</i> refusal without the framework needing to know consumer 058 * concepts exist. 059 * 060 * <h5 class='section'>Reserved async terminal outcomes</h5> 061 * <p> 062 * {@link Outcome#CANCELLED cancelled} and {@link Outcome#CANCELLED_AFTER_EFFECT cancelled-after-effect} are 063 * <b>reserved from day one</b> (as {@code VIEW_META} reserved {@code rowActions} before it was implemented), even 064 * though a synchronous write never emits them. A future async/streaming variant can therefore emit them without a 065 * second result-contract bump and a three-sided lockstep. 066 * 067 * <h5 class='section'>Example:</h5> 068 * <p class='bjava'> 069 * <jc>// A success carrying the authoritative row read back from the remote system.</jc> 070 * ActionResult <jv>ok</jv> = ActionResult.<jsm>success</jsm>(<jv>incidentReadBack</jv>); 071 * 072 * <jc>// A named refusal from the consumer's write gate.</jc> 073 * ActionResult <jv>no</jv> = ActionResult.<jsm>refusal</jsm>(<js>"write-guard:not-armed"</js>); 074 * 075 * <jc>// A recorded prior outcome, labelled as a replay rather than re-calling.</jc> 076 * ActionResult <jv>again</jv> = ActionResult.<jsm>success</jsm>(<jv>incidentReadBack</jv>).replay(<jk>true</jk>); 077 * </p> 078 * 079 * <h5 class='section'>See Also:</h5> 080 * <ul> 081 * <li class='jc'>{@link RowAction} 082 * <li class='jc'>{@link org.apache.juneau.console.widgets.ModalDef} 083 * <li class='jc'>{@link IdempotencyKey} 084 * </ul> 085 * 086 * @since 10.0.0 087 */ 088@BeanType(properties="contractVersion,outcome,replay,refusalCode,message,row,resultForm") 089@SuppressWarnings({ 090 "java:S1845" // Fluent-builder setters intentionally mirror field names (Juneau DSL convention). 091}) 092public class ActionResult { 093 094 /** 095 * The frozen action-result contract version — its <b>own</b> discriminator, deliberately NOT aliased to 096 * {@code VIEW_META}. Bumped only on a breaking wire change to this contract. 097 */ 098 public static final String CONTRACT_VERSION = "1"; 099 100 /** 101 * The outcome discriminator a row-action submit reports. 102 * 103 * <p> 104 * Each constant carries the lowercase wire token emitted for the {@code outcome} field. The four synchronous 105 * outcomes ({@link #SUCCESS}, {@link #FAILURE}, {@link #REFUSAL}, {@link #UNKNOWN}) are what a synchronous write 106 * can emit; {@link #CANCELLED} and {@link #CANCELLED_AFTER_EFFECT} are reserved for a future async variant 107 * and are frozen here so that contract does not force a second result-contract bump. 108 */ 109 public enum Outcome { 110 111 /** The write completed and {@link ActionResult#row} carries the authoritative row read back. */ 112 SUCCESS("success"), 113 114 /** The write was attempted and failed. */ 115 FAILURE("failure"), 116 117 /** The write was refused; {@link ActionResult#refusalCode} carries the named reason. */ 118 REFUSAL("refusal"), 119 120 /** The write's outcome is genuinely unknown (e.g. a crash mid-write) — an honest, non-optimistic state. */ 121 UNKNOWN("unknown"), 122 123 /** Reserved for a future async variant: the job was cancelled before any effect. */ 124 CANCELLED("cancelled"), 125 126 /** Reserved for a future async variant: the job was cancelled after a partial/uncertain effect. */ 127 CANCELLED_AFTER_EFFECT("cancelled-after-effect"); 128 129 private final String wire; 130 131 Outcome(String wire) { 132 this.wire = wire; 133 } 134 135 /** 136 * Returns the lowercase wire token for this outcome. 137 * 138 * @return The wire token (e.g. {@code "success"}). 139 */ 140 public String wire() { 141 return wire; 142 } 143 } 144 145 /** The frozen contract-version discriminator (always {@value #CONTRACT_VERSION} for this contract). */ 146 public String contractVersion = CONTRACT_VERSION; 147 148 /** The outcome discriminator wire token (see {@link Outcome#wire()}). */ 149 public String outcome; 150 151 /** 152 * Whether this result is a recorded prior outcome replayed rather than re-executed; omitted from the wire when 153 * unset (the common, non-replay case). 154 */ 155 public Boolean replay; 156 157 /** 158 * The opaque, namespaced refusal code for a {@link Outcome#REFUSAL refusal}; omitted from the wire for any other 159 * outcome. Framework codes come from 160 * {@link org.apache.juneau.rest.server.filter.BoundaryReason#name()}; consumer codes are namespaced 161 * {@code app:} / {@code write-guard:} strings (see the class javadoc). 162 */ 163 public String refusalCode; 164 165 /** Optional human-readable supplementary message for any outcome; omitted from the wire when unset. */ 166 public String message; 167 168 /** 169 * The authoritative row payload the runtime re-renders the row from (the {@code MERGE_ROW} case); omitted from 170 * the wire when unset. This is the row as the remote system actually reports it, not what the request asked for. 171 */ 172 public Object row; 173 174 /** 175 * The follow-up <b>read-only</b> form-source URL a receipt is painted from; omitted from the wire when unset. 176 * 177 * <h5 class='section'>Honored on {@link Outcome#SUCCESS success} only</h5> 178 * <p> 179 * This is the mechanical form of <i>a receipt must never look like a receipt for a write that didn't happen</i>. 180 * On a failure, a refusal, an unknown, or a transport failure this field is <b>ignored</b>, no follow-up GET is 181 * issued, and nothing is swapped — so no result host can appear for a write that did not commit. 182 * 183 * <h5 class='section'>The served payload must not carry a form</h5> 184 * <p> 185 * The URL serves a {@code ModalDef}, and that payload <b>must not</b> declare a {@code form}. A result host is 186 * terminal by definition: the write already committed, so there is nothing left to gate. A payload carrying 187 * inputs is refused <i>visibly</i> rather than rendered read-only or silently stripped, because it is a consumer 188 * authoring bug and should be visible as one. 189 * 190 * <h5 class='section'>It is a swap, not a new dialog</h5> 191 * <p> 192 * The runtime paints the receipt into the dialog that is <b>already open</b>, at the same layer depth, and only 193 * when that dialog opted in with {@code ModalDef.keepOpenOnSubmit}. Set on a result whose dialog did not opt in, 194 * the swap is dropped, the success is reported unchanged, and the runtime emits one non-alarming diagnostic 195 * rather than either opening a layer or making a successful write look failed. 196 */ 197 public String resultForm; 198 199 /** 200 * Starts a {@link Outcome#SUCCESS success} result carrying the authoritative row read back from the remote system. 201 * 202 * @param row The authoritative row payload to re-render from. Can be <jk>null</jk> (e.g. a navigate/redraw 203 * action that carries no row). 204 * @return A new {@link ActionResult}. 205 */ 206 public static ActionResult success(Object row) { 207 var r = new ActionResult(); 208 r.outcome = Outcome.SUCCESS.wire(); 209 r.row = row; 210 return r; 211 } 212 213 /** 214 * Readability alias for {@link #success(Object) success(null)} — a {@link Outcome#SUCCESS success} result 215 * that carries no row (e.g. a delete, or a navigate/redraw action). 216 * 217 * <h5 class='section'>Example:</h5> 218 * <p class='bjava'> 219 * <jc>// Equivalent to ActionResult.success(null).message(...)</jc> 220 * <jk>return</jk> ActionResult.<jsm>ok</jsm>().message(<js>"Deleted"</js>); 221 * </p> 222 * 223 * @return A new {@link ActionResult}. 224 */ 225 public static ActionResult ok() { 226 return success(null); 227 } 228 229 /** 230 * Readability alias for {@link #success(Object)}. 231 * 232 * <h5 class='section'>Example:</h5> 233 * <p class='bjava'> 234 * <jk>return</jk> ActionResult.<jsm>ok</jsm>(<jv>store</jv>.updatePet(<jv>pet</jv>)).message(<js>"Saved"</js>); 235 * </p> 236 * 237 * @param row The authoritative row payload to re-render from. Can be <jk>null</jk>. 238 * @return A new {@link ActionResult}. 239 */ 240 public static ActionResult ok(Object row) { 241 return success(row); 242 } 243 244 /** 245 * Starts a {@link Outcome#FAILURE failure} result. 246 * 247 * @return A new {@link ActionResult}. 248 */ 249 public static ActionResult failure() { 250 var r = new ActionResult(); 251 r.outcome = Outcome.FAILURE.wire(); 252 return r; 253 } 254 255 /** 256 * Starts a {@link Outcome#REFUSAL refusal} result carrying the named, opaque refusal code. 257 * 258 * @param refusalCode The opaque, namespaced refusal code. Must not be <jk>null</jk> or blank (a refusal with no 259 * code is exactly the free-text-message anti-pattern this contract forbids). 260 * @return A new {@link ActionResult}. 261 * @throws IllegalArgumentException If {@code refusalCode} is <jk>null</jk> or blank. 262 */ 263 public static ActionResult refusal(String refusalCode) { 264 if (ib(refusalCode)) 265 throw iaex("ActionResult refusalCode must not be null or blank."); 266 var r = new ActionResult(); 267 r.outcome = Outcome.REFUSAL.wire(); 268 r.refusalCode = refusalCode; 269 return r; 270 } 271 272 /** 273 * Starts an {@link Outcome#UNKNOWN unknown} result — an honest, non-optimistic terminal state. 274 * 275 * @return A new {@link ActionResult}. 276 */ 277 public static ActionResult unknown() { 278 var r = new ActionResult(); 279 r.outcome = Outcome.UNKNOWN.wire(); 280 return r; 281 } 282 283 /** 284 * Sets the authoritative row payload to re-render from. 285 * 286 * @param value The new value. Can be <jk>null</jk> to unset. 287 * @return This object. 288 */ 289 public ActionResult row(Object value) { 290 row = value; 291 return this; 292 } 293 294 /** 295 * Marks this result as a recorded prior outcome replayed rather than re-executed. 296 * 297 * @param value <jk>true</jk> to mark this a replay; <jk>false</jk>/<jk>null</jk> to unset (omitting it from the wire). 298 * @return This object. 299 */ 300 public ActionResult replay(Boolean value) { 301 replay = (value != null && value) ? Boolean.TRUE : null; 302 return this; 303 } 304 305 /** 306 * Sets the optional human-readable supplementary message. 307 * 308 * @param value The new value. Can be <jk>null</jk> to unset. 309 * @return This object. 310 */ 311 public ActionResult message(String value) { 312 message = value; 313 return this; 314 } 315 316 /** 317 * Sets the opaque, namespaced refusal code (see {@link #refusalCode}). 318 * 319 * @param value The new value. Can be <jk>null</jk> to unset. 320 * @return This object. 321 */ 322 public ActionResult refusalCode(String value) { 323 refusalCode = value; 324 return this; 325 } 326 327 /** 328 * Sets the follow-up read-only form-source URL a receipt is painted from (see {@link #resultForm}). 329 * 330 * @param value The new value. Can be <jk>null</jk> to unset. 331 * @return This object. 332 */ 333 public ActionResult resultForm(String value) { 334 resultForm = value; 335 return this; 336 } 337}