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 — no new field, no fail-loud contract bump — 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 — the same {@code servlet:} convention {@code ViewsMixin} uses for its asset URLs. {@link #streamUrl} 072 * and {@link #cancelUrl} are {@link Uri @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}