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 * ![WebConsole Ready Event Sequence](AddToHead.svg) 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 */ 108@SuppressWarnings("PMD.LinguisticNaming") 109public class AddPageResources extends ConsoleCommand { 110 111 private final List<ScriptResource> scriptResources = new ArrayList<>(); 112 private final List<URI> cssUris = new ArrayList<>(); 113 private String cssSource; 114 115 /** 116 * Add the URI of a JavaScript resource that is to be added to the 117 * header section of the web console page. 118 * 119 * @param scriptResource the resource to add 120 * @return the event for easy chaining 121 */ 122 public AddPageResources addScriptResource(ScriptResource scriptResource) { 123 scriptResources.add(scriptResource); 124 return this; 125 } 126 127 /** 128 * Return all script URIs 129 * 130 * @return the result 131 */ 132 public ScriptResource[] scriptResources() { 133 return scriptResources.toArray(new ScriptResource[0]); 134 } 135 136 /** 137 * Add the URI of a CSS resource that is to be added to the 138 * header section of the web console page. 139 * 140 * @param uri the URI 141 * @return the event for easy chaining 142 */ 143 public AddPageResources addCss(URI uri) { 144 cssUris.add(uri); 145 return this; 146 } 147 148 /** 149 * Return all CSS URIs. 150 * 151 * @return the result 152 */ 153 public URI[] cssUris() { 154 return cssUris.toArray(new URI[0]); 155 } 156 157 /** 158 * @return the cssSource 159 */ 160 public String cssSource() { 161 return cssSource; 162 } 163 164 /** 165 * @param cssSource the cssSource to set 166 */ 167 public AddPageResources setCssSource(String cssSource) { 168 this.cssSource = cssSource; 169 return this; 170 } 171 172 @Override 173 public void emitJson(Writer writer) throws IOException { 174 emitJson(writer, "addPageResources", Arrays.stream(cssUris()).map( 175 URI::toString).toArray(String[]::new), 176 cssSource(), scriptResources()); 177 } 178 179 /** 180 * Represents a script resource that is to be loaded or evaluated 181 * by the browser. Note that a single instance can either be used 182 * for a URI or inline JavaScript, not for both. 183 */ 184 @JsonSerialize(using = ScriptResource.Serializer.class) 185 public static class ScriptResource { 186 private static final String[] EMPTY_ARRAY = new String[0]; 187 188 private URI scriptUri; 189 private String scriptId; 190 private String scriptType; 191 private String scriptSource; 192 private String[] provides = EMPTY_ARRAY; 193 private String[] requires = EMPTY_ARRAY; 194 195 /** 196 * @return the scriptUri to be loaded 197 */ 198 public URI scriptUri() { 199 return scriptUri; 200 } 201 202 /** 203 * Sets the scriptUri to to be loaded, clears the `scriptSource` 204 * attribute. 205 * 206 * @param scriptUri the scriptUri to to be loaded 207 * @return this object for easy chaining 208 */ 209 public ScriptResource setScriptUri(URI scriptUri) { 210 this.scriptUri = scriptUri; 211 return this; 212 } 213 214 /** 215 * Gets the script type (defaults to no type). 216 * 217 * @return the script type 218 */ 219 public String getScriptType() { 220 return scriptType; 221 } 222 223 /** 224 * Sets the script type. 225 * 226 * @param scriptType the new script type 227 * @return the script resource 228 */ 229 public ScriptResource setScriptType(String scriptType) { 230 this.scriptType = scriptType; 231 return this; 232 } 233 234 /** 235 * Gets the script id (defaults to no id). 236 * 237 * @return the script type 238 */ 239 public String getScriptId() { 240 return scriptId; 241 } 242 243 /** 244 * Sets the script id. 245 * 246 * @param scriptId the script id 247 * @return the script resource 248 */ 249 public ScriptResource setScriptId(String scriptId) { 250 this.scriptId = scriptId; 251 return this; 252 } 253 254 /** 255 * @return the script source 256 */ 257 public String scriptSource() { 258 return scriptSource; 259 } 260 261 /** 262 * Sets the script source to evaluate. Clears the 263 * `scriptUri` attribute. 264 * 265 * @param scriptSource the scriptSource to set 266 * @return this object for easy chaining 267 */ 268 public ScriptResource setScriptSource(String scriptSource) { 269 this.scriptSource = scriptSource; 270 scriptUri = null; 271 return this; 272 } 273 274 /** 275 * Loads the script source to evaluate. Clears the 276 * `scriptUri` attribute. Closes the reader. 277 * 278 * @param in the input stream 279 * @return this object for easy chaining 280 * @throws IOException 281 */ 282 @SuppressWarnings({ "PMD.ShortVariable", "PMD.PreserveStackTrace" }) 283 public ScriptResource loadScriptSource(Reader in) throws IOException { 284 try (BufferedReader buffered = new BufferedReader(in)) { 285 this.scriptSource 286 = buffered.lines().collect(Collectors.joining("\r\n")); 287 } catch (UncheckedIOException e) { 288 throw e.getCause(); 289 } 290 scriptUri = null; 291 return this; 292 } 293 294 /** 295 * Returns the list of JavaScript features that this 296 * script resource provides. 297 * 298 * @return the list of features 299 */ 300 public String[] provides() { 301 return Arrays.copyOf(provides, provides.length); 302 } 303 304 /** 305 * Sets the list of JavaScript features that this 306 * script resource provides. For commonly available 307 * JavaScript libraries, it is recommended to use 308 * their home page URL (without the protocol part) as 309 * feature name. 310 * 311 * @param provides the list of features 312 * @return this object for easy chaining 313 */ 314 public ScriptResource setProvides(String... provides) { 315 this.provides = Arrays.copyOf(provides, provides.length); 316 return this; 317 } 318 319 /** 320 * Returns the list of JavaScript features that this 321 * script resource requires. 322 * 323 * @return the list of features 324 */ 325 public String[] requires() { 326 return Arrays.copyOf(requires, requires.length); 327 } 328 329 /** 330 * Sets the list of JavaScript features that this 331 * script resource requires. 332 * 333 * @param requires the list of features 334 * @return this object for easy chaining 335 */ 336 public ScriptResource setRequires(String... requires) { 337 this.requires = Arrays.copyOf(requires, requires.length); 338 return this; 339 } 340 341 /** 342 * The Class Serializer. 343 */ 344 @SuppressWarnings("serial") 345 public static class Serializer extends StdSerializer<ScriptResource> { 346 347 /** 348 * Instantiates a new serializer. 349 */ 350 public Serializer() { 351 super((Class<ScriptResource>) null); 352 } 353 354 /** 355 * Instantiates a new serializer. 356 * 357 * @param type the type 358 */ 359 public Serializer(Class<ScriptResource> type) { 360 super(type); 361 } 362 363 /** 364 * Serialize. 365 * 366 * @param obj the obj 367 * @param generator the generator 368 * @param ctx the ctx 369 * @throws IOException Signals that an I/O exception has occurred. 370 */ 371 @Override 372 public void serialize(ScriptResource obj, JsonGenerator generator, 373 SerializerProvider ctx) throws IOException { 374 generator.writeStartObject(); 375 if (obj.scriptUri != null) { 376 generator.writeStringField("uri", obj.scriptUri.toString()); 377 } 378 if (obj.scriptId != null) { 379 generator.writeStringField("id", obj.scriptId); 380 } 381 if (obj.scriptType != null) { 382 generator.writeStringField("type", obj.scriptType); 383 } 384 if (obj.scriptSource != null) { 385 generator.writeStringField("source", obj.scriptSource); 386 } 387 generator.writeArrayFieldStart("requires"); 388 for (String req : obj.requires) { 389 generator.writeString(req); 390 } 391 generator.writeEndArray(); 392 generator.writeArrayFieldStart("provides"); 393 for (String prov : obj.provides) { 394 generator.writeString(prov); 395 } 396 generator.writeEndArray(); 397 generator.writeEndObject(); 398 } 399 } 400 } 401}