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
019
020import org.apache.juneau.commons.bean.*;
021import org.apache.juneau.marshall.*;
022import static org.apache.juneau.commons.utils.Shorts.*;
023
024/**
025 * The "job accepted" envelope a row action's submit returns <b>instead of</b> a terminal {@link ActionResult} when
026 * the write is asynchronous.
027 *
028 * <h5 class='section'>Why this is not a new {@code RowAction} wire field</h5>
029 * <p>
030 * Whether an action is asynchronous is a property of the <b>response</b>, not of the declared action: the same
031 * {@link RowAction} POST returns either a terminal {@link ActionResult} (synchronous) or this pointer (asynchronous).
032 * So the {@code RowAction} wire schema is untouched &mdash; no new field, no fail-loud contract bump &mdash; and the
033 * {@code juneau-views.js} runtime routes to the streaming path purely by recognizing a {@link #streamUrl} in the 2xx
034 * body.  It carries no data to render, only capability pointers, so it needs no version handshake of its own.
035 *
036 * <h5 class='section'>The URLs are capabilities</h5>
037 * <p>
038 * {@link #streamUrl} points at the SSE progress stream and {@link #cancelUrl} at the cancel endpoint; both embed the
039 * job's unguessable {@value AsyncJobRegistry#CAPABILITY_BITS}-bit id (see {@link AsyncJob}), which is the whole of the
040 * access control.  The browser {@code EventSource} that consumes {@link #streamUrl} cannot set a CSRF header,
041 * which is precisely why the id must be unguessable rather than CSRF-gated.
042 *
043 * <h5 class='section'>See Also:</h5>
044 * <ul>
045 *    <li class='jc'>{@link AsyncJobsMixin}
046 *    <li class='jc'>{@link AsyncJob}
047 *    <li class='jc'>{@link ActionResult}
048 * </ul>
049 *
050 * @since 10.0.0
051 */
052@BeanType(properties="jobId,streamUrl,cancelUrl")
053@SuppressWarnings({
054   "java:S1845" // Fluent-builder setters intentionally mirror field names (Juneau DSL convention).
055})
056public class AsyncJobRef {
057
058   /** The job's unguessable capability id. */
059   public String jobId;
060
061   /** The URL of the SSE progress stream (a browser {@code EventSource} target); embeds {@link #jobId}. */
062   @Uri
063   public String streamUrl;
064
065   /** The URL of the cancel endpoint (a non-safe POST); embeds {@link #jobId}. */
066   @Uri
067   public String cancelUrl;
068
069   /**
070    * Builds a reference to the given job using {@code servlet:}-relative URLs against the {@link AsyncJobsMixin}
071    * mount &mdash; the same {@code servlet:} convention {@code ViewsMixin} uses for its asset URLs. {@link #streamUrl}
072    * and {@link #cancelUrl} are {@link Uri &#64;Uri}-annotated, which is what opts them into resolution: Juneau's
073    * serializer only rewrites the {@code servlet:} pseudo-scheme for a property whose {@code ClassMeta} or
074    * {@code BeanPropertyMeta} reports {@code isUri()==true}, and a plain {@code String} field reports that only
075    * via an explicit {@code @Uri} (see {@code MarshalledPropertyPostProcessor}); without it the literal
076    * {@code "servlet:..."} string would reach the wire unresolved.  With the annotation present, every REST
077    * response serializer (JSON, HTML, XML, ...) resolves it against the current request's {@code UriContext}.
078    *
079    * @param job The started job.  Must not be <jk>null</jk>.
080    * @return A new reference carrying the job's id and its {@code servlet:}-relative stream + cancel URLs.
081    */
082   public static AsyncJobRef of(AsyncJob job) {
083      reqnn("job", job);
084      var r = new AsyncJobRef();
085      r.jobId = job.id();
086      r.streamUrl = "servlet:" + AsyncJobsMixin.streamPath(job.id());
087      r.cancelUrl = "servlet:" + AsyncJobsMixin.cancelPath(job.id());
088      return r;
089   }
090
091   /**
092    * Builds a reference carrying explicit stream/cancel URLs (for a consumer that resolves URLs itself, e.g. a
093    * template-rendered surface downstream of the {@code servlet:} rewrite).
094    *
095    * @param jobId The job's capability id.  Must not be <jk>null</jk> or blank.
096    * @param streamUrl The SSE stream URL.  Must not be <jk>null</jk> or blank.
097    * @param cancelUrl The cancel URL.  Must not be <jk>null</jk> or blank.
098    * @return A new reference.
099    */
100   public static AsyncJobRef of(String jobId, String streamUrl, String cancelUrl) {
101      var r = new AsyncJobRef();
102      r.jobId = jobId;
103      r.streamUrl = streamUrl;
104      r.cancelUrl = cancelUrl;
105      return r;
106   }
107}