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 &mdash; an {@link Outcome outcome discriminator}, the
030 * authoritative {@link #row} data to re-render from, a {@link #replay} marker, and a {@link #refusalCode} &mdash; 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 &mdash; 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 &mdash; 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) &mdash; 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 &mdash; 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)} &mdash; 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 &mdash; 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}