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.freemarker; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020import static org.apache.juneau.console.freemarker.ToolkitPack.Kind.*; 021 022import java.util.*; 023import java.util.function.UnaryOperator; 024import java.util.logging.*; 025 026import org.apache.juneau.rest.server.*; 027import org.apache.juneau.console.*; 028import org.apache.juneau.rest.server.staticfile.*; 029import org.apache.juneau.terminal.region.*; 030import org.apache.juneau.console.views.*; 031import org.apache.juneau.console.widgets.*; 032 033/** 034 * Registry of named toolkit packs for the {@code <@page toolkit=...>} directive and for the packs cards require. 035 * 036 * <p> 037 * Built-in packs: 038 * <ul> 039 * <li>{@code "jquery"}, {@code "datatables"} and {@code "datatables-buttons"} ({@link ToolkitPack.Kind#VENDOR}) — 040 * WebJars, resolved through {@link #WEBJAR_RESOLVER} to the host's {@link WebJarsMixin} mount. 041 * {@code "datatables"} depends on {@code "jquery"}; {@code "datatables-buttons"} on {@code "datatables"}. 042 * <li>{@code "datatables-glue"} ({@link ToolkitPack.Kind#VENDOR}) — the first-party glue 043 * ({@link ViewsMixin#DATATABLES_JS_PATH}, served by {@code DataTablesMixin}); depends on {@code "datatables"}. 044 * Every {@code <@card type="datatables">} requires it. 045 * <li>{@code "views"} ({@link ToolkitPack.Kind#RUNTIME}) — the views runtime. JS order is a contract: bus (first), renders, 046 * icons, search, pagestate, urlstate, ribbon, views, config, regions, terminal-view, helpers. 047 * {@code juneau-urlstate.js} must precede {@code juneau-views.js} / {@code juneau-config.js}. 048 * <li>{@code "calendar"} ({@link ToolkitPack.Kind#RUNTIME}) — the {@code juneau-calendar.*} runtime from 049 * {@code juneau-console-widgets}, resolved through {@link #WIDGETS_RESOLVER}. 050 * <li>{@code "xterm"} ({@link ToolkitPack.Kind#VENDOR}) — the xterm.js WebJar (library and stylesheet), resolved 051 * through {@link #WEBJAR_RESOLVER}. 052 * <li>{@code "terminal"} ({@link ToolkitPack.Kind#RUNTIME}) — the {@code juneau-terminal.*} runtime from 053 * {@code juneau-terminal-region}, resolved through {@link TerminalMixin#terminalAssetUrl(RestRequest, String)}; 054 * depends on {@code "views"} and {@code "xterm"}. Every {@code <@card type="terminal">} requires it, so a page 055 * with a terminal card needs no {@code toolkit=}. 056 * <li>{@code "charts"} ({@link ToolkitPack.Kind#RUNTIME}) — {@code juneau-charts.js} from 057 * {@code juneau-console}, resolved through {@link ConsoleChromeMixin#consoleAssetUrl(RestRequest, String)}; 058 * depends on {@code "echarts"} and {@code "data"}. Every {@code <@card type="chart">} requires it. 059 * <li>{@code "joblog"} ({@link ToolkitPack.Kind#RUNTIME}) — {@code juneau-joblog.js} from 060 * {@code juneau-console}; no dependencies. Every {@code <@card type="job-log">} requires it. 061 * <li>{@code "echarts"} ({@link ToolkitPack.Kind#VENDOR}) — the ECharts WebJar ({@link #ECHARTS_JS_ASSET}), 062 * optional on the classpath: a missing jar is E-70 at startup when the app opts in 063 * ({@code ConsoleFreemarkerMixin.Builder.requirePacks}), else at the first chart page. 064 * <li>{@code "echarts-gl"} ({@link ToolkitPack.Kind#VENDOR}) — the echarts-gl WebJar; depends on {@code "echarts"}; 065 * a page opts in with {@code toolkit("echarts-gl")}. 066 * <li>{@code "data"} ({@link ToolkitPack.Kind#RUNTIME}) — {@code juneau-data.js} ({@code JuneauViews.data}); depends on 067 * {@code "views"}. 068 * </ul> 069 * 070 * <p> 071 * {@link #resolve(List, RestRequest, List, String)} expands the requested packs depth-first over 072 * {@link ToolkitPack#dependsOn()} (each pack after its dependencies, siblings in request order, each pack once), skips 073 * {@linkplain #provide(Collection) provided} packs (their dependencies still load), skips an asset whose file name the 074 * page already loads through {@code init=} or {@code css=} (with a WARN), and splits the result by kind. 075 * {@link #validate()} checks the static graph: unknown {@code dependsOn} names, cycles, and a VENDOR pack depending on 076 * a RUNTIME pack. 077 * 078 * @since 10.0.0 079 */ 080@SuppressWarnings({ 081 "java:S1192" // Repeated literals are contract wire keys; named constants would obscure the contract shape. 082}) 083public final class ToolkitPackRegistry { 084 085 private static final Logger LOG = Logger.getLogger(ToolkitPackRegistry.class.getName()); 086 087 /** The built-in views runtime pack. */ 088 public static final String PACK_VIEWS = "views"; 089 090 /** 091 * The built-in {@code "calendar"} pack: the already-shipping {@code juneau-calendar.*} runtime from 092 * {@code juneau-console-widgets}. Never inferred from {@code type="calendar"}; a calendar page lists it 093 * explicitly (e.g. {@code toolkit="views,calendar"}). 094 */ 095 public static final String PACK_CALENDAR = "calendar"; 096 097 /** The built-in jQuery WebJar pack. */ 098 public static final String PACK_JQUERY = "jquery"; 099 100 /** The built-in DataTables WebJar pack (library plus default styling CSS). */ 101 public static final String PACK_DATATABLES = "datatables"; 102 103 /** The built-in pack holding the first-party DataTables glue. */ 104 public static final String PACK_DATATABLES_GLUE = "datatables-glue"; 105 106 /** The built-in DataTables Buttons WebJar pack (copy and CSV export, no JSZip or pdfmake). */ 107 public static final String PACK_DATATABLES_BUTTONS = "datatables-buttons"; 108 109 /** The built-in xterm.js WebJar pack (library plus stylesheet). */ 110 public static final String PACK_XTERM = "xterm"; 111 112 /** The built-in terminal runtime pack; depends on {@code "views"} and {@code "xterm"}. */ 113 public static final String PACK_TERMINAL = "terminal"; 114 115 /** The built-in chart-card runtime pack ({@code juneau-charts.js}); depends on {@code "echarts"} and {@code "data"}. */ 116 public static final String PACK_CHARTS = "charts"; 117 118 /** The built-in job-log card runtime pack ({@code juneau-joblog.js}); no dependencies. */ 119 public static final String PACK_JOBLOG = "joblog"; 120 121 /** Dashboard chrome ({@code PageSpec.dashboard}); added by the builder, never needed in {@code toolkit(...)}. */ 122 public static final String PACK_DASHBOARD = "dashboard"; 123 124 /** The built-in ECharts WebJar pack ({@code org.webjars.npm:echarts}, optional on the classpath). */ 125 public static final String PACK_ECHARTS = "echarts"; 126 127 /** The built-in echarts-gl WebJar pack; depends on {@code "echarts"}; never required by a card type. */ 128 public static final String PACK_ECHARTS_GL = "echarts-gl"; 129 130 /** The built-in DataRegistry runtime pack ({@code juneau-data.js}); depends on {@code "views"}. */ 131 public static final String PACK_DATA = "data"; 132 133 private static final String NPM = "org.webjars.npm"; 134 135 /** The ECharts library asset id, for {@link #WEBJAR_RESOLVER}. */ 136 public static final String ECHARTS_JS_ASSET = WebJarResolver.asset(NPM, "echarts", "echarts/{version}/dist/echarts.min.js"); 137 138 /** The echarts-gl library asset id (path unverified against the WebJar; no module depends on it). */ 139 public static final String ECHARTS_GL_JS_ASSET = WebJarResolver.asset(NPM, "echarts-gl", "echarts-gl/{version}/dist/echarts-gl.min.js"); 140 141 static final String E70 = "chart cards need the ECharts WebJar org.webjars.npm:echarts, which is not on the classpath; add it to the application's pom (version property 'webjars-echarts.version')"; 142 static final String E70_PACK = "toolkit pack '%s' needs the WebJar %s, which is not on the classpath; add it to the application's pom (version property '%s')"; 143 144 // groupId:artifactId -> the version property the message names. 145 // echarts is not listed: it has its own message (E70) in webJarMissing. 146 private static final Map<String,String> WEBJAR_PROPERTIES = Map.of( 147 NPM + ":echarts-gl", "webjars-echarts-gl.version"); 148 149 /** Resolves an asset path to an absolute, cache-busted URL against the in-flight request. */ 150 @FunctionalInterface 151 public interface AssetUrlResolver { 152 /** 153 * @param req The in-flight request. 154 * @param path The asset path. 155 * @return The resolved, cache-busted URL. 156 */ 157 String resolve(RestRequest req, String path); 158 } 159 160 /** The default resolver: {@link ViewsMixin#viewAssetUrl(RestRequest, String)}. */ 161 public static final AssetUrlResolver VIEWS_RESOLVER = ViewsMixin::viewAssetUrl; 162 163 /** The {@code "calendar"} pack resolver: {@link WidgetsMixin#widgetAssetUrl(RestRequest, String)}. */ 164 public static final AssetUrlResolver WIDGETS_RESOLVER = WidgetsMixin::widgetAssetUrl; 165 166 /** The WebJar resolver: paths are {@link WebJarResolver#asset(String, String, String)} ids. */ 167 public static final AssetUrlResolver WEBJAR_RESOLVER = WebJarResolver::resolve; 168 169 /** 170 * Resolved absolute URLs, split by pack kind. 171 * 172 * @param vendorCss VENDOR CSS, emitted before the page {@code css=}. 173 * @param vendorJs VENDOR JS, emitted before the runtime JS. 174 * @param runtimeCss RUNTIME CSS, emitted after the page {@code css=}. 175 * @param runtimeJs RUNTIME JS, emitted before the {@code after-toolkit} slot. 176 */ 177 public record Resolved(List<String> vendorCss, List<String> vendorJs, List<String> runtimeCss, List<String> runtimeJs) { 178 /** Nothing to load. */ 179 public static final Resolved EMPTY = new Resolved(List.of(), List.of(), List.of(), List.of()); 180 } 181 182 private final Map<String,ToolkitPack> packs = new LinkedHashMap<>(); 183 private final Set<String> provided = new LinkedHashSet<>(); 184 185 /** Constructs the registry with the built-in packs installed. */ 186 public ToolkitPackRegistry() { 187 register(ToolkitPack.create(PACK_JQUERY).kind(VENDOR).resolver(WEBJAR_RESOLVER) 188 .js(WebJarResolver.asset("org.webjars", "jquery", "jquery/{version}/jquery.min.js")) 189 .build()); 190 register(ToolkitPack.create(PACK_DATATABLES).kind(VENDOR).resolver(WEBJAR_RESOLVER) 191 .css(WebJarResolver.asset(NPM, "datatables.net-dt", "datatables.net-dt/{version}/css/dataTables.dataTables.min.css")) 192 .js(WebJarResolver.asset(NPM, "datatables.net", "datatables.net/{version}/js/dataTables.min.js")) 193 .dependsOn(PACK_JQUERY) 194 .build()); 195 register(ToolkitPack.create(PACK_DATATABLES_GLUE).kind(VENDOR).resolver(VIEWS_RESOLVER) 196 .js(ViewsMixin.DATATABLES_JS_PATH) 197 .dependsOn(PACK_DATATABLES) 198 .build()); 199 register(ToolkitPack.create(PACK_DATATABLES_BUTTONS).kind(VENDOR).resolver(WEBJAR_RESOLVER) 200 .css(WebJarResolver.asset(NPM, "datatables.net-buttons-dt", "datatables.net-buttons-dt/{version}/css/buttons.dataTables.min.css")) 201 .js( 202 WebJarResolver.asset(NPM, "datatables.net-buttons", "datatables.net-buttons/{version}/js/dataTables.buttons.min.js"), 203 WebJarResolver.asset(NPM, "datatables.net-buttons", "datatables.net-buttons/{version}/js/buttons.html5.min.js")) 204 .dependsOn(PACK_DATATABLES) 205 .build()); 206 register(ToolkitPack.create(PACK_VIEWS).kind(RUNTIME).resolver(VIEWS_RESOLVER) 207 .css(ViewsMixin.VIEWS_CSS_PATH, ViewsMixin.CONFIG_CSS_PATH) 208 .js( 209 // Dependency-free and read by views, regions and the console shell: it must load first (E-JS-46). 210 ViewsMixin.BUS_JS_PATH, 211 ViewsMixin.RENDERS_JS_PATH, 212 ViewsMixin.ICONS_JS_PATH, 213 ViewsMixin.SEARCH_JS_PATH, 214 ViewsMixin.PAGESTATE_JS_PATH, 215 ViewsMixin.URLSTATE_JS_PATH, 216 ViewsMixin.RIBBON_JS_PATH, 217 ViewsMixin.VIEWS_JS_PATH, 218 ViewsMixin.CONFIG_JS_PATH, 219 ViewsMixin.REGIONS_JS_PATH, 220 ViewsMixin.RUN_VIEW_JS_PATH, 221 ViewsMixin.HELPERS_JS_PATH) 222 .build()); 223 // The "calendar" pack ships from juneau-console-widgets and resolves through that mixin's own 224 // cache-busting helper, not VIEWS_RESOLVER. A page that wants it lists toolkit="views,calendar" so 225 // juneau-views.js still loads before juneau-calendar.js (shared layer stack). 226 register(ToolkitPack.create(PACK_CALENDAR).kind(RUNTIME).resolver(WIDGETS_RESOLVER) 227 .css(WidgetsMixin.CALENDAR_CSS_PATH) 228 .js(WidgetsMixin.CALENDAR_JS_PATH) 229 .build()); 230 register(ToolkitPack.create(PACK_XTERM).kind(VENDOR).resolver(WEBJAR_RESOLVER) 231 .css(TerminalMixin.XTERM_CSS_ASSET) 232 .js(TerminalMixin.XTERM_JS_ASSET) 233 .build()); 234 register(ToolkitPack.create(PACK_TERMINAL).kind(RUNTIME).resolver(TerminalMixin::terminalAssetUrl) 235 .css(TerminalMixin.CSS_PATH) 236 .js(TerminalMixin.JS_PATH) 237 .dependsOn(PACK_VIEWS, PACK_XTERM) 238 .build()); 239 register(ToolkitPack.create(PACK_ECHARTS).kind(VENDOR).resolver(WEBJAR_RESOLVER) 240 .js(ECHARTS_JS_ASSET) 241 .build()); 242 register(ToolkitPack.create(PACK_ECHARTS_GL).kind(VENDOR).resolver(WEBJAR_RESOLVER) 243 .js(ECHARTS_GL_JS_ASSET) 244 .dependsOn(PACK_ECHARTS) 245 .build()); 246 register(ToolkitPack.create(PACK_DATA).kind(RUNTIME).resolver(VIEWS_RESOLVER) 247 .js(ViewsMixin.DATA_JS_PATH) 248 .dependsOn(PACK_VIEWS) 249 .build()); 250 register(ToolkitPack.create(PACK_CHARTS).kind(RUNTIME).resolver(ConsoleChromeMixin::consoleAssetUrl) 251 .js(ConsoleChromeMixin.CHARTS_JS_PATH) 252 .dependsOn(PACK_ECHARTS, PACK_DATA) 253 .build()); 254 register(ToolkitPack.create(PACK_JOBLOG).kind(RUNTIME).resolver(ConsoleChromeMixin::consoleAssetUrl) 255 .js(ConsoleChromeMixin.JOBLOG_JS_PATH) 256 .build()); 257 register(ToolkitPack.create(PACK_DASHBOARD).kind(RUNTIME).resolver(ConsoleChromeMixin::consoleAssetUrl) 258 .js(ConsoleChromeMixin.DASHBOARD_JS_PATH) 259 .dependsOn(PACK_VIEWS, PACK_DATA) 260 .build()); 261 } 262 263 /** 264 * Registers a pack, replacing any pack with the same name (all of its fields, including {@code dependsOn}). 265 * 266 * @param pack The pack. 267 */ 268 public synchronized void register(ToolkitPack pack) { 269 packs.put(pack.name(), pack); 270 } 271 272 /** 273 * Marks packs as already loaded by the app: they contribute no URLs, but their dependencies still load. 274 * 275 * @param names The pack names. 276 * @return This object. 277 */ 278 public synchronized ToolkitPackRegistry provide(Collection<String> names) { 279 provided.addAll(names); 280 return this; 281 } 282 283 /** 284 * @param name A pack name. 285 * @return <jk>true</jk> if a pack with that name is registered. 286 */ 287 public synchronized boolean contains(String name) { 288 return packs.containsKey(name); 289 } 290 291 /** 292 * Checks the static graph: every {@code dependsOn} and provided name is registered, there is no cycle, and no 293 * VENDOR pack depends on a RUNTIME pack. 294 * 295 * @return This object. 296 * @throws IllegalArgumentException On the first problem found. 297 */ 298 public synchronized ToolkitPackRegistry validate() { 299 for (var pack : packs.values()) { 300 for (var dep : pack.dependsOn()) { 301 var target = packs.get(dep); 302 if (n(target)) 303 throw iaex("Unknown toolkit pack '%s' (dependsOn of pack '%s').", dep, pack.name()); 304 if (pack.kind() == VENDOR && target.kind() == RUNTIME) 305 throw iaex("VENDOR toolkit pack '%s' cannot depend on RUNTIME pack '%s'.", pack.name(), dep); 306 } 307 } 308 for (var name : provided) 309 if (! packs.containsKey(name)) 310 throw iaex("Unknown toolkit pack '%s' (providedPacks).", name); 311 expand(packs.keySet(), "registered"); 312 return this; 313 } 314 315 /** 316 * Checks that every name is a registered pack. 317 * 318 * @param names The pack names. 319 * @param source Where the names came from, for the message (e.g. {@code <@page toolkit=>}). 320 * @throws IllegalArgumentException If a name is unknown. 321 */ 322 public synchronized void requireKnown(Collection<String> names, String source) { 323 for (var name : names) 324 if (! packs.containsKey(name)) 325 throw iaex("Unknown toolkit pack '%s' (%s).", name, source); 326 } 327 328 /** 329 * @param root A registered pack name. 330 * @param target A pack name. 331 * @return <jk>true</jk> if {@code target} is {@code root} or one of its transitive {@code dependsOn}. 332 */ 333 public synchronized boolean reaches(String root, String target) { 334 return expand(List.of(root), "reaches").stream().anyMatch(p -> p.name().equals(target)); 335 } 336 337 /** 338 * Probes, at startup, every WebJar the given packs reach: each non-provided VENDOR pack resolved through 339 * {@link #WEBJAR_RESOLVER}. 340 * 341 * @param roots Root pack names; empty probes nothing. 342 * @return This object. 343 * @throws IllegalStateException E-70 when a WebJar or its file is not on the classpath. 344 */ 345 public ToolkitPackRegistry checkWebJars(Collection<String> roots) { 346 return checkWebJars(roots, WebJarResolver::resolvePath); 347 } 348 349 // Package-private: the probe is injectable because the echarts jar is on every test classpath. 350 synchronized ToolkitPackRegistry checkWebJars(Collection<String> roots, UnaryOperator<String> probe) { 351 for (var pack : expand(new LinkedHashSet<>(roots), "requirePacks")) { 352 if (provided.contains(pack.name()) || pack.kind() != VENDOR || pack.resolver() != WEBJAR_RESOLVER) 353 continue; 354 for (var asset : concat(pack.cssPaths(), pack.jsPaths())) { 355 try { 356 probe.apply(asset); 357 } catch (IllegalStateException e) { 358 var x = webJarMissing(pack, asset, e); 359 throw x != null ? x : new IllegalStateException("Pack '" + pack.name() + "' " + e.getMessage(), e); 360 } 361 } 362 } 363 return this; 364 } 365 366 private static List<String> concat(List<String> a, List<String> b) { 367 var out = new ArrayList<String>(a); 368 out.addAll(b); 369 return out; 370 } 371 372 // Distinguishes "jar absent" from "jar present, file missing" (the resolver's own message says which). 373 private static String causeSuffix(RuntimeException cause) { 374 return nn(cause.getMessage()) ? " [" + cause.getMessage() + "]" : ""; 375 } 376 377 // E-70 for a WebJar the message table knows; null when the asset is not a known WebJar. 378 private static IllegalStateException webJarMissing(ToolkitPack pack, String asset, RuntimeException cause) { 379 var parts = asset.split(":", 3); 380 var coords = parts.length == 3 ? parts[0] + ":" + parts[1] : asset; 381 if (eq(coords, NPM + ":echarts")) 382 return new IllegalStateException("E-70: " + E70 + causeSuffix(cause), cause); 383 var prop = WEBJAR_PROPERTIES.get(coords); 384 if (prop == null) 385 return null; 386 return new IllegalStateException("E-70: " + f(E70_PACK, pack.name(), coords, prop) + causeSuffix(cause), cause); 387 } 388 389 /** 390 * Resolves the root packs and their dependencies to absolute URLs, split by kind. 391 * 392 * @param names The root pack names, in order. Empty or <jk>null</jk> = nothing (no request needed). 393 * @param req The in-flight request. 394 * @param pageUrls The page's own {@code init=} and {@code css=} URLs; a pack asset with the same file name is 395 * skipped with a WARN. 396 * @param template The page template name, for the WARN. 397 * @return The resolved URLs. 398 * @throws IllegalArgumentException On an unknown pack or a cycle. 399 * @throws IllegalStateException If the request is missing or a resolver fails (e.g. a WebJar isn't on the classpath). 400 */ 401 public Resolved resolve(List<String> names, RestRequest req, List<String> pageUrls, String template) { 402 if (n(names) || names.isEmpty()) 403 return Resolved.EMPTY; 404 if (n(req)) 405 throw isex("<@page toolkit> needs FreemarkerRenderScope.request() (renderer wrap)."); 406 List<ToolkitPack> order; 407 Set<String> skip; 408 synchronized (this) { 409 order = expand(new LinkedHashSet<>(names), "requested"); 410 skip = Set.copyOf(provided); 411 } 412 var pageFiles = new LinkedHashMap<String,String>(); 413 for (var url : pageUrls) 414 pageFiles.putIfAbsent(fileName(url), url); 415 var vendorCss = new ArrayList<String>(); 416 var vendorJs = new ArrayList<String>(); 417 var runtimeCss = new ArrayList<String>(); 418 var runtimeJs = new ArrayList<String>(); 419 for (var pack : order) { 420 if (skip.contains(pack.name())) 421 continue; 422 var vendor = pack.kind() == VENDOR; 423 addUrls(pack, pack.cssPaths(), vendor ? vendorCss : runtimeCss, req, pageFiles, template); 424 addUrls(pack, pack.jsPaths(), vendor ? vendorJs : runtimeJs, req, pageFiles, template); 425 } 426 return new Resolved(List.copyOf(vendorCss), List.copyOf(vendorJs), List.copyOf(runtimeCss), List.copyOf(runtimeJs)); 427 } 428 429 /** 430 * The file name of a URL: the last path segment, without query or fragment, lower-cased. 431 * 432 * @param url The URL. 433 * @return The file name. 434 */ 435 static String fileName(String url) { 436 var s = url; 437 var q = s.indexOf('?'); 438 if (q >= 0) 439 s = s.substring(0, q); 440 var h = s.indexOf('#'); 441 if (h >= 0) 442 s = s.substring(0, h); 443 return s.substring(s.lastIndexOf('/') + 1).toLowerCase(Locale.ROOT); 444 } 445 446 private static void addUrls(ToolkitPack pack, List<String> paths, List<String> out, RestRequest req, Map<String,String> pageFiles, String template) { 447 for (var path : paths) { 448 String url; 449 try { 450 url = pack.resolver().resolve(req, path); 451 } catch (IllegalStateException | IllegalArgumentException e) { 452 var x = e instanceof IllegalStateException ? webJarMissing(pack, path, e) : null; 453 throw x != null ? x : new IllegalStateException("Pack '" + pack.name() + "' " + e.getMessage(), e); 454 } 455 var dup = pageFiles.get(fileName(url)); 456 if (nn(dup)) { 457 LOG.log(Level.WARNING, () -> f("Template '%s': skipped pack '%s' asset %s; the page already loads %s.", template, pack.name(), url, dup)); 458 continue; 459 } 460 out.add(url); 461 } 462 } 463 464 // Caller holds the monitor. 465 private List<ToolkitPack> expand(Collection<String> roots, String source) { 466 var out = new LinkedHashMap<String,ToolkitPack>(); 467 for (var name : roots) 468 visit(name, source, new ArrayList<>(), out); 469 return List.copyOf(out.values()); 470 } 471 472 private void visit(String name, String source, List<String> path, Map<String,ToolkitPack> out) { 473 if (out.containsKey(name)) 474 return; 475 var at = path.indexOf(name); 476 if (at >= 0) { 477 var cycle = tl(path.subList(at, path.size())); 478 cycle.add(name); 479 throw new IllegalArgumentException("Toolkit pack cycle: " + String.join(" → ", cycle) + "."); 480 } 481 var pack = packs.get(name); 482 if (n(pack)) 483 throw iaex("Unknown toolkit pack '%s' (%s).", name, source); 484 path.add(name); 485 for (var dep : pack.dependsOn()) 486 visit(dep, "dependsOn of pack '" + name + "'", path, out); 487 path.remove(path.size() - 1); 488 out.put(name, pack); 489 } 490}