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.widgets;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import org.apache.juneau.http.*;
022import org.apache.juneau.http.response.*;
023import org.apache.juneau.rest.server.*;
024import org.apache.juneau.rest.server.processor.*;
025import org.apache.juneau.rest.server.util.*;
026
027/**
028 * The widget toolkit's serving-path mixin: the mount, asset-URL API and serving endpoints for the widget-owned
029 * browser runtime ({@code juneau-calendar.js}, {@code juneau-calendar.css}), plus the
030 * contract-version handshake constants those runtimes bake in.
031 *
032 * <p>
033 * Compose into a host resource via {@link Rest#mixins() @Rest(mixins=WidgetsMixin.class)}; the asset URLs then become
034 * available alongside the host's own endpoints.
035 *
036 * <h5 class='section'>These assets still need the view runtime beside them</h5>
037 * <p>
038 * The bytes live here, but they are <b>not</b> a standalone widget runtime: the calendar script
039 * resolves its glyphs through the rich-view module's icon registry ({@code juneau-icons.js}) and pushes its
040 * popovers onto the ONE shared layer stack that {@code juneau-views.js} publishes.  A page that loads
041 * this script must therefore <b>also</b> load the view module's {@code juneau-icons.js} and
042 * {@code juneau-views.js}.  "Widgets without views" is deliberately not a goal here; what this module owns is the
043 * bean contracts and the bytes, not independence from views.
044 *
045 * <p>
046 * For backwards compatibility the rich-view mixin keeps <b>deprecated</b> accessors at the same paths it served
047 * these assets from before they moved, reading the bytes from <b>this</b> module's classpath.  An existing
048 * application therefore keeps working unchanged; a new one composes this mixin.
049 *
050 * <h5 class='section'>Contract-version handshake:</h5>
051 * <p>
052 * Each {@code *_CONTRACT_VERSION} constant is the handshake stamp the matching runtime bakes in.
053 * {@link #CALENDAR_CONTRACT_VERSION} aliases {@link CalendarDef#CONTRACT_VERSION};
054 * {@link #BAR_CONTRACT_VERSION} aliases {@link BarSlot#CONTRACT_VERSION};
055 * {@link #HEADER_CONTRACT_VERSION} is a literal (the header bean is gone). The three are deliberately distinct from one another: a calendar-envelope
056 * revision must never force a header or bar-sidecar bump, or vice-versa.
057 *
058 * <h5 class='section'>Cache-busting + versioned URLs:</h5>
059 * <p>
060 * {@link #widgetAssetUrl(String)} carries the same {@code ?v=<buildVersion>-<hash8>} content-sensitive cache-buster
061 * shape the views mixin uses: an 8-hex-char content hash of the asset's own served bytes, computed once and cached.
062 * Keying the buster off content rather than the version string alone matters for {@code -SNAPSHOT} builds, whose
063 * version is stable across dev rebuilds.  The hashing itself is the shared
064 * {@link org.apache.juneau.commons.utils.ChecksumUtils#hash8(byte[])} function reached through
065 * {@link ClasspathAssetCache}; this module holds its <b>own</b> cache instance anchored on this class, so its
066 * {@code buildVersion} resolves this module's implementation version rather than a neighbouring module's.  The two
067 * modules share the hash function, not a cache.
068 *
069 * <h5 class='section'>The serving-path {@code Widget.validate()} gate</h5>
070 * <p>
071 * This class also nests {@link WidgetValidationProcessor}, a {@link ResponseProcessor} that calls
072 * {@link Widget#validate()} on any REST response content that is a {@link Widget}, fail-closed, ahead of every
073 * byte-writing processor. It is registered two ways &mdash; both belt-and-braces, not redundant duplicates, since
074 * {@code ResponseProcessorList} de-duplicates by class:
075 * <ul>
076 *    <li>Via {@code ServiceLoader} ({@code META-INF/services/org.apache.juneau.rest.server.processor.ResponseProcessor}),
077 *       so it applies to <b>every</b> resource on a classpath that carries this module, with no consumer opt-in.
078 *    <li>Via this class's own {@link Rest#responseProcessors() @Rest(responseProcessors=...)} +
079 *       {@link Rest#mergeResponseProcessorsIntoHost() mergeResponseProcessorsIntoHost=true}, so a host that composes
080 *       this mixin is covered even if its classpath loses the services file (a shading/repackaging step, for example).
081 * </ul>
082 * It is declared as a <b>nested</b> class here &mdash; rather than a new top-level file &mdash; because
083 * {@code Widgets_ModuleBoundary_Test.b01_onlyTheMixinReferencesTheRestServingFramework} allowlists exactly this file
084 * as the module's single exception to "the beans stay independent of the REST serving framework"; a response
085 * processor is itself a serving-framework type, so nesting it here keeps that rule literally true rather than
086 * requiring an amendment.
087 *
088 * <h5 class='section'>See Also:</h5>
089 * <ul>
090 *    <li class='jc'>{@link CalendarDef}
091 *    <li class='jc'>{@link WidgetValidationProcessor}
092 * </ul>
093 *
094 * @since 10.0.0
095 */
096// @formatter:off
097@Rest(responseProcessors=WidgetsMixin.WidgetValidationProcessor.class, mergeResponseProcessorsIntoHost=true)
098public class WidgetsMixin {
099
100   /**
101    * The URL path at which the reusable-calendar runtime is served (relative to the host mount).
102    *
103    * <h5 class='section'>Load order is a contract, not a preference:</h5>
104    * <p>
105    * This {@code <script>} MUST come after the view module's {@code juneau-views.js}.  The calendar's
106    * {@code "+N more"} popover is pushed onto the ONE shared layer stack that script publishes, so Escape, focus
107    * return and z-order match every other layer in the page.  The calendar deliberately defines no second stack of
108    * its own: if the shared one is absent it fails loud rather than opening a popover nothing can dismiss.
109    */
110   public static final String CALENDAR_JS_PATH = "/juneau-calendar.js";
111
112   /** The URL path at which the reusable-calendar stylesheet is served (relative to the host mount). */
113   public static final String CALENDAR_CSS_PATH = "/juneau-calendar.css";
114
115   /**
116    * The per-month calendar-event envelope contract-version handshake constant that the calendar runtime bakes in,
117    * aliased from the value the calendar model emits ({@link CalendarDef#CONTRACT_VERSION}).
118    */
119   public static final String CALENDAR_CONTRACT_VERSION = CalendarDef.CONTRACT_VERSION;
120
121   /**
122    * The app-header refresh-envelope contract-version handshake constant that the console shell bakes in.
123    */
124   public static final String HEADER_CONTRACT_VERSION = "1";
125
126   /**
127    * The bar-slot refresh-envelope contract-version handshake constant that the console shell bakes in, aliased
128    * from the value the bar model emits ({@link BarSlot#CONTRACT_VERSION}).
129    */
130   public static final String BAR_CONTRACT_VERSION = BarSlot.CONTRACT_VERSION;
131
132   /** Classpath location of the shipped reusable-calendar runtime. */
133   static final String CALENDAR_JS_RESOURCE = "/org/apache/juneau/widgets/juneau-calendar.js";
134
135   /** Classpath location of the shipped reusable-calendar stylesheet. */
136   static final String CALENDAR_CSS_RESOURCE = "/org/apache/juneau/widgets/juneau-calendar.css";
137
138   /** Content type emitted for the JavaScript assets. */
139   static final String JS_CONTENT_TYPE = "text/javascript;charset=utf-8";
140
141   /** Content type emitted for the stylesheet asset. */
142   static final String CSS_CONTENT_TYPE = "text/css;charset=utf-8";
143
144   /** {@code Cache-Control} header emitted for every asset (1 day). */
145   static final String CACHE_CONTROL = "max-age=86400, public";
146
147   /**
148    * Read+cache+hash helper for this module's shipped assets, anchored on this class so
149    * {@link ClasspathAssetCache#buildVersion()} resolves this module's own implementation version (see that class's
150    * javadoc's version-anchor section).  Independent of any other module's cache by construction.
151    */
152   private static final ClasspathAssetCache ASSET_CACHE = new ClasspathAssetCache(WidgetsMixin.class);
153
154   /**
155    * [GET /juneau-calendar.js] &mdash; serve the reusable-calendar runtime.
156    *
157    * @return The calendar runtime as a JavaScript {@link HttpResource}.
158    */
159   @RestGet(
160      path=CALENDAR_JS_PATH,
161      summary="Juneau reusable-calendar runtime",
162      description="First-party, opt-in JavaScript that hydrates a data-juneau-calendar month grid from its seed sidecar or a same-origin per-month GET.",
163      swagger=@OpSwagger(ignore=true)
164   )
165   public HttpResource getCalendarScript() {
166      return serve(CALENDAR_JS_RESOURCE, JS_CONTENT_TYPE);
167   }
168
169   /**
170    * [GET /juneau-calendar.css] &mdash; serve the reusable-calendar stylesheet.
171    *
172    * @return The calendar stylesheet as a CSS {@link HttpResource}.
173    */
174   @RestGet(
175      path=CALENDAR_CSS_PATH,
176      summary="Juneau reusable-calendar stylesheet",
177      description="First-party, opt-in CSS for the reusable-calendar month grid, chips, legend and day popover; declares --jc-* token fallbacks.",
178      swagger=@OpSwagger(ignore=true)
179   )
180   public HttpResource getCalendarStylesheet() {
181      return serve(CALENDAR_CSS_RESOURCE, CSS_CONTENT_TYPE);
182   }
183
184   /**
185    * Returns the servlet-relative URL for a widget asset served by this mixin, carrying a
186    * {@code ?v=<buildVersion>-<hash8>} content-sensitive cache-buster suitable for a page's {@code head=} block.
187    *
188    * @param path One of the asset path constants ({@link #CALENDAR_JS_PATH},
189    *    {@link #CALENDAR_CSS_PATH}).
190    * @return The servlet-relative asset URL with the version+content-hash cache-buster appended.
191    * @throws IllegalArgumentException If this mixin does not ship the bytes for the given path.
192    */
193   public static String widgetAssetUrl(String path) {
194      return assetUrl(path, resourceFor(path));
195   }
196
197   /**
198    * Returns a real, browser-fetchable <b>absolute</b> URL for a widget asset served by this mixin, resolved against
199    * the given request's context path and mount &mdash; carrying the same cache-buster as
200    * {@link #widgetAssetUrl(String)}.
201    *
202    * <p>
203    * {@link #widgetAssetUrl(String)} returns a {@code servlet:}-prefixed URL that only Juneau's own HTML serializer
204    * resolves.  A template-rendering consumer sits downstream of that serializer, so it never sees the rewrite and
205    * would otherwise receive the literal, unfetchable string.  This overload resolves the URL itself, per-request.
206    *
207    * <p>
208    * The path is validated before the request is touched, so an unserved path is rejected identically with or
209    * without a live request.
210    *
211    * @param req The current request, supplying the context path/mount to resolve against.
212    * @param path One of the asset path constants ({@link #CALENDAR_JS_PATH},
213    *    {@link #CALENDAR_CSS_PATH}).
214    * @return The absolute asset URL with the version+content-hash cache-buster appended.
215    * @throws IllegalArgumentException If this mixin does not ship the bytes for the given path.
216    */
217   public static String widgetAssetUrl(RestRequest req, String path) {
218      return assetUrl(req, path, resourceFor(path));
219   }
220
221   /** Composes the servlet-relative URL + cache-buster for a classpath asset shipped by this module. */
222   static String assetUrl(String path, String classpathResource) {
223      return "servlet:" + path + ASSET_CACHE.cacheBuster(classpathResource);
224   }
225
226   /** Composes the request-resolved absolute URL + cache-buster for a classpath asset shipped by this module. */
227   static String assetUrl(RestRequest req, String path, String classpathResource) {
228      return req.getUriResolver().resolve("servlet:" + path) + ASSET_CACHE.cacheBuster(classpathResource);
229   }
230
231   /** Reads (and caches) the classpath asset and wraps it as a cacheable {@link HttpResource}. */
232   private static HttpResource serve(String resource, String contentType) {
233      return ASSET_CACHE.serve(resource, contentType, CACHE_CONTROL);
234   }
235
236   /**
237    * Maps a public asset path constant to the classpath resource this module ships for it.
238    *
239    * <p>
240    * A new asset gets its entry here and its {@code @RestGet} accessor in the same change, so the mount and the
241    * bytes always arrive together and no declared path can resolve to an empty response.
242    */
243   private static String resourceFor(String path) {
244      if (eq(path, CALENDAR_JS_PATH)) return CALENDAR_JS_RESOURCE;
245      if (eq(path, CALENDAR_CSS_PATH)) return CALENDAR_CSS_RESOURCE;
246      throw iaex("Widget asset ''%s'' is not served by this mixin.", path);
247   }
248
249   /**
250    * The serving-path {@link Widget#validate()} gate: fail-closed structural validation for any
251    * REST response content that is a {@link Widget}, giving the JSON serving path the same gate the
252    * {@code juneau-console-views} HTML emitters already give every {@code Widget} they consume.
253    *
254    * <h5 class='section'>Why this is a {@link ResponseProcessor}, not a {@code RestContext} hook</h5>
255    * <p>
256    * {@code juneau-rest-server} does not (and must not) depend on this module, so a hard-coded validation step
257    * inside it cannot see {@link Widget} without inverting that dependency. A {@link ResponseProcessor} declared
258    * here, discovered by every classpath that carries this module via {@code ServiceLoader}, needs no such
259    * reflection or hoisted marker interface &mdash; see the class Javadoc.
260    *
261    * <h5 class='section'>Why {@code NEXT}, never {@code FINISHED}</h5>
262    * <p>
263    * This processor validates and declines to handle: it never claims the response, so it cannot perturb chain
264    * ordering, content-type negotiation, or the {@code NotImplemented} fallthrough. Non-{@link Widget} content
265    * (including the unrelated {@code examples.views.Widget} demo row bean and the legacy
266    * {@code org.apache.juneau.rest.server.widget.Widget} SVL resolver &mdash; see the module Javadoc's naming
267    * collision note) costs one {@code instanceof} test and is otherwise untouched.
268    *
269    * <h5 class='section'>Why {@code InternalServerError} (500), not the caller's problem</h5>
270    * <p>
271    * A malformed {@link Widget} reaching the serving path is a <b>server</b> defect &mdash; the client asked for
272    * a dialog correctly and the server built a broken one &mdash; not an inbound-validation failure of the kind
273    * {@code ValidationException} (400) is scoped to. The underlying {@link IllegalArgumentException} rides as the
274    * cause so the message names the failing rule.
275    *
276    * <h5 class='section'>Deliberately not deduplicated against {@link ModalDef#checked()}/{@link FormDef#checked()}</h5>
277    * <p>
278    * A widget that already called {@code checked()} is validated twice per response: once at build time, once
279    * here. No {@code validated} flag is added to skip the second pass &mdash; a mutable "already validated" flag
280    * on a wire bean would re-introduce an order-dependence bug in the validation dimension (mutate-after-checked)
281    * that this module's contract deliberately avoids elsewhere. {@code validate()} is cheap and idempotent, so the
282    * repeat costs nothing worth building around.
283    *
284    * <h5 class='section'>Traversal is top-level only</h5>
285    * <p>
286    * Only {@code res.getContent()} itself is tested. Nested widgets are covered by the beans' own recursive
287    * {@code validate()} (e.g. {@link ModalDef#validate()} &rarr; {@link FormDef#validate()}); a {@link Widget}
288    * inside a {@code Collection}/{@code Map} envelope is not walked into &mdash; no endpoint in this tree returns
289    * that shape, and walking one would collide with the unrelated {@code examples.views.Widget} row-bean lists
290    * some example endpoints do return.
291    *
292    * <h5 class='section'>Escape hatch</h5>
293    * <p>
294    * A resource that must serve a deliberately-invalid {@link Widget} can override the response-processor chain
295    * entirely with a {@code @Bean ResponseProcessorList} factory method, which replaces the whole list. There is
296    * no narrower, dedicated opt-out.
297    *
298    * @since 10.0.0
299    */
300   public static class WidgetValidationProcessor implements ResponseProcessor {
301
302      @Override /* ResponseProcessor */
303      public int process(RestOpSession opSession) {
304         var o = opSession.getResponse().getContent().orElse(null);
305         if (o instanceof Widget w) {
306            try {
307               w.validate();
308            } catch (IllegalArgumentException e) {
309               throw new InternalServerError(e, "Malformed %s in response: %s", w.getClass().getSimpleName(), e.getMessage());
310            }
311         }
312         return NEXT;  // Validate and decline; the normal chain serializes.
313      }
314   }
315}