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 — 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 — rather than a new top-level file — 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] — 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] — 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 — 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 — 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 — 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 — the client asked for 272 * a dialog correctly and the server built a broken one — 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 — 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()} → {@link FormDef#validate()}); a {@link Widget} 288 * inside a {@code Collection}/{@code Map} envelope is not walked into — 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}