001/* 002 * JGrapes Event Driven Framework 003 * Copyright (C) 2017-2018 Michael N. Lipp 004 * 005 * This program is free software; you can redistribute it and/or modify it 006 * under the terms of the GNU Affero General Public License as published by 007 * the Free Software Foundation; either version 3 of the License, or 008 * (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, but 011 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 012 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 013 * for more details. 014 * 015 * You should have received a copy of the GNU Affero General Public License along 016 * with this program; if not, see <http://www.gnu.org/licenses/>. 017 */ 018 019package org.jgrapes.webconsole.base.events; 020 021import com.fasterxml.jackson.core.JsonGenerator; 022import com.fasterxml.jackson.databind.SerializerProvider; 023import com.fasterxml.jackson.databind.annotation.JsonSerialize; 024import com.fasterxml.jackson.databind.ser.std.StdSerializer; 025import java.io.BufferedReader; 026import java.io.IOException; 027import java.io.Reader; 028import java.io.UncheckedIOException; 029import java.io.Writer; 030import java.net.URI; 031import java.util.ArrayList; 032import java.util.Arrays; 033import java.util.List; 034import java.util.stream.Collectors; 035import org.jgrapes.webconsole.base.PageResourceProvider; 036import org.jgrapes.webconsole.base.freemarker.FreeMarkerConsoleWeblet; 037 038/** 039 * Adds {@code <link .../>}, `<style>...</style>` or 040 * `<script ...></script>` nodes to the web console's 041 * `<head>` node on behalf of components that provide such resources. 042 * 043 * Adding resource references causes the browser to issue `GET` request that 044 * (usually) refer to resources that are then provided by the component 045 * that created the {@link AddPageResources} event. 046 * 047 * The sequence of events is shown in the diagram. 048 * 049 *  050 * 051 * See {@link ResourceRequest} for details about the processing 052 * of the {@link PageResourceRequest}. 053 * 054 * The `GET` request may also, of course, refer to a resource from 055 * another server and thus not result in a {@link PageResourceRequest}. 056 * 057 * Adding a `<script src=...></script>` node to a document's `<head>` 058 * causes the referenced JavaScript to be loaded asynchronously. This 059 * can cause problems if a dynamically added library relies on another 060 * library to be available. Script resources are therefore specified using 061 * the {@link ScriptResource} class, which allows to specify loading 062 * dependencies between resources. The code in the browser delays the 063 * addition of a `<script>` node until all other script resources that 064 * it depends on are loaded. 065 * 066 * Some libraries provided as page resources may already be required by the 067 * JavaScript web console code (especially by the resource manager that handles 068 * the delayed loading). They can therefore not be loaded by this 069 * mechanism, which depends on the web console code. Such page resources 070 * may be "pre-loaded" by adding the appropriate `script` element to the 071 * initial web console page. In order to make the pre-loading known to the 072 * resource manager, the `script` elements must carry an attribute 073 * `data-jgwc-provides` with a comma separated list of JavaScript resource 074 * names provided by loading the script resource. The name(s) must match 075 * the name(s) used in the {@link AddPageResources} request generated 076 * by the {@link PageResourceProvider} for the pre-loaded resource(s). 077 * Here's an example (for a web console using the 078 * {@link FreeMarkerConsoleWeblet} to generate the initial web console page): 079 * ```html 080 * {@code <}script data-jgwc-provides="jquery" 081 * src="${renderSupport.pageResource('jquery/jquery' + minifiedExtension + '.js')}"{@code >} 082 * {@code <}/script{@code >}; 083 * ``` 084 * 085 * @startuml AddToHead.svg 086 * hide footbox 087 * 088 * activate Browser 089 * Browser -> WebConsole: "consoleReady" 090 * deactivate Browser 091 * activate WebConsole 092 * WebConsole -> PageResourceProvider: ConsoleReady 093 * activate PageResourceProvider 094 * PageResourceProvider -> WebConsole: AddPageResources 095 * deactivate PageResourceProvider 096 * WebConsole -> Browser: "addPageResource" 097 * deactivate WebConsole 098 * activate Browser 099 * deactivate WebConsole 100 * Browser -> WebConsole: "GET <page resource URI>" 101 * activate WebConsole 102 * WebConsole -> PageResourceProvider: PageResourceRequest 103 * deactivate WebConsole 104 * activate PageResourceProvider 105 * deactivate PageResourceProvider 106 * @enduml 107 */ 108public class AddPageResources extends ConsoleCommand { 109 110 private final List<ScriptResource> scriptResources = new ArrayList<>(); 111 private final List<URI> cssUris = new ArrayList<>(); 112 private String cssSource; 113 114 /** 115 * Add the URI of a JavaScript resource that is to be added to the 116 * header section of the web console page. 117 * 118 * @param scriptResource the resource to add 119 * @return the event for easy chaining 120 */ 121 public AddPageResources addScriptResource(ScriptResource scriptResource) { 122 scriptResources.add(scriptResource); 123 return this; 124 } 125 126 /** 127 * Return all script URIs 128 * 129 * @return the result 130 */ 131 public ScriptResource[] scriptResources() { 132 return scriptResources.toArray(new ScriptResource[0]); 133 } 134 135 /** 136 * Add the URI of a CSS resource that is to be added to the 137 * header section of the web console page. 138 * 139 * @param uri the URI 140 * @return the event for easy chaining 141 */ 142 public AddPageResources addCss(URI uri) { 143 cssUris.add(uri); 144 return this; 145 } 146 147 /** 148 * Return all CSS URIs. 149 * 150 * @return the result 151 */ 152 public URI[] cssUris() { 153 return cssUris.toArray(new URI[0]); 154 } 155 156 /** 157 * @return the cssSource 158 */ 159 public String cssSource() { 160 return cssSource; 161 } 162 163 /** 164 * @param cssSource the cssSource to set 165 */ 166 public AddPageResources setCssSource(String cssSource) { 167 this.cssSource = cssSource; 168 return this; 169 } 170 171 @Override 172 public void emitJson(Writer writer) throws IOException { 173 emitJson(writer, "addPageResources", Arrays.stream(cssUris()).map( 174 URI::toString).toArray(String[]::new), 175 cssSource(), scriptResources()); 176 } 177 178 /** 179 * Represents a script resource that is to be loaded or evaluated 180 * by the browser. Note that a single instance can either be used 181 * for a URI or inline JavaScript, not for both. 182 */ 183 @JsonSerialize(using = ScriptResource.Serializer.class) 184 public static class ScriptResource { 185 private static final String[] EMPTY_ARRAY = new String[0]; 186 187 private URI scriptUri; 188 private String scriptId; 189 private String scriptType; 190 private String scriptSource; 191 private String[] provides = EMPTY_ARRAY; 192 private String[] requires = EMPTY_ARRAY; 193 194 /** 195 * @return the scriptUri to be loaded 196 */ 197 public URI scriptUri() { 198 return scriptUri; 199 } 200 201 /** 202 * Sets the scriptUri to to be loaded, clears the `scriptSource` 203 * attribute. 204 * 205 * @param scriptUri the scriptUri to to be loaded 206 * @return this object for easy chaining 207 */ 208 public ScriptResource setScriptUri(URI scriptUri) { 209 this.scriptUri = scriptUri; 210 return this; 211 } 212 213 /** 214 * Gets the script type (defaults to no type). 215 * 216 * @return the script type 217 */ 218 public String getScriptType() { 219 return scriptType; 220 } 221 222 /** 223 * Sets the script type. 224 * 225 * @param scriptType the new script type 226 * @return the script resource 227 */ 228 public ScriptResource setScriptType(String scriptType) { 229 this.scriptType = scriptType; 230 return this; 231 } 232 233 /** 234 * Gets the script id (defaults to no id). 235 * 236 * @return the script type 237 */ 238 public String getScriptId() { 239 return scriptId; 240 } 241 242 /** 243 * Sets the script id. 244 * 245 * @param scriptId the script id 246 * @return the script resource 247 */ 248 public ScriptResource setScriptId(String scriptId) { 249 this.scriptId = scriptId; 250 return this; 251 } 252 253 /** 254 * @return the script source 255 */ 256 public String scriptSource() { 257 return scriptSource; 258 } 259 260 /** 261 * Sets the script source to evaluate. Clears the 262 * `scriptUri` attribute. 263 * 264 * @param scriptSource the scriptSource to set 265 * @return this object for easy chaining 266 */ 267 public ScriptResource setScriptSource(String scriptSource) { 268 this.scriptSource = scriptSource; 269 scriptUri = null; 270 return this; 271 } 272 273 /** 274 * Loads the script source to evaluate. Clears the 275 * `scriptUri` attribute. Closes the reader. 276 * 277 * @param in the input stream 278 * @return this object for easy chaining 279 * @throws IOException 280 */ 281 @SuppressWarnings({ "PMD.ShortVariable", "PMD.PreserveStackTrace" }) 282 public ScriptResource loadScriptSource(Reader in) throws IOException { 283 try (BufferedReader buffered = new BufferedReader(in)) { 284 this.scriptSource 285 = buffered.lines().collect(Collectors.joining("\r\n")); 286 } catch (UncheckedIOException e) { 287 throw e.getCause(); 288 } 289 scriptUri = null; 290 return this; 291 } 292 293 /** 294 * Returns the list of JavaScript features that this 295 * script resource provides. 296 * 297 * @return the list of features 298 */ 299 public String[] provides() { 300 return Arrays.copyOf(provides, provides.length); 301 } 302 303 /** 304 * Sets the list of JavaScript features that this 305 * script resource provides. For commonly available 306 * JavaScript libraries, it is recommended to use 307 * their home page URL (without the protocol part) as 308 * feature name. 309 * 310 * @param provides the list of features 311 * @return this object for easy chaining 312 */ 313 public ScriptResource setProvides(String... provides) { 314 this.provides = Arrays.copyOf(provides, provides.length); 315 return this; 316 } 317 318 /** 319 * Returns the list of JavaScript features that this 320 * script resource requires. 321 * 322 * @return the list of features 323 */ 324 public String[] requires() { 325 return Arrays.copyOf(requires, requires.length); 326 } 327 328 /** 329 * Sets the list of JavaScript features that this 330 * script resource requires. 331 * 332 * @param requires the list of features 333 * @return this object for easy chaining 334 */ 335 public ScriptResource setRequires(String... requires) { 336 this.requires = Arrays.copyOf(requires, requires.length); 337 return this; 338 } 339 340 /** 341 * The Class Serializer. 342 */ 343 @SuppressWarnings("serial") 344 public static class Serializer extends StdSerializer<ScriptResource> { 345 346 /** 347 * Instantiates a new serializer. 348 */ 349 public Serializer() { 350 super((Class<ScriptResource>) null); 351 } 352 353 /** 354 * Instantiates a new serializer. 355 * 356 * @param type the type 357 */ 358 public Serializer(Class<ScriptResource> type) { 359 super(type); 360 } 361 362 /** 363 * Serialize. 364 * 365 * @param obj the obj 366 * @param generator the generator 367 * @param ctx the ctx 368 * @throws IOException Signals that an I/O exception has occurred. 369 */ 370 @Override 371 public void serialize(ScriptResource obj, JsonGenerator generator, 372 SerializerProvider ctx) throws IOException { 373 generator.writeStartObject(); 374 if (obj.scriptUri != null) { 375 generator.writeStringField("uri", obj.scriptUri.toString()); 376 } 377 if (obj.scriptId != null) { 378 generator.writeStringField("id", obj.scriptId); 379 } 380 if (obj.scriptType != null) { 381 generator.writeStringField("type", obj.scriptType); 382 } 383 if (obj.scriptSource != null) { 384 generator.writeStringField("source", obj.scriptSource); 385 } 386 generator.writeArrayFieldStart("requires"); 387 for (String req : obj.requires) { 388 generator.writeString(req); 389 } 390 generator.writeEndArray(); 391 generator.writeArrayFieldStart("provides"); 392 for (String prov : obj.provides) { 393 generator.writeString(prov); 394 } 395 generator.writeEndArray(); 396 generator.writeEndObject(); 397 } 398 } 399 } 400}