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 java.io.BufferedReader; 022import java.io.IOException; 023import java.io.Reader; 024import java.io.UncheckedIOException; 025import java.io.Writer; 026import java.net.URI; 027import java.util.ArrayList; 028import java.util.Arrays; 029import java.util.List; 030import java.util.stream.Collectors; 031import org.jdrupes.json.JsonArray; 032import org.jdrupes.json.JsonObject; 033import org.jgrapes.webconsole.base.PageResourceProvider; 034import org.jgrapes.webconsole.base.freemarker.FreeMarkerConsoleWeblet; 035 036/** 037 * Adds {@code <link .../>}, `<style>...</style>` or 038 * `<script ...></script>` nodes to the web console's 039 * `<head>` node on behalf of components that provide such resources. 040 * 041 * Adding resource references causes the browser to issue `GET` request that 042 * (usually) refer to resources that are then provided by the component 043 * that created the {@link AddPageResources} event. 044 * 045 * The sequence of events is shown in the diagram. 046 * 047 * ![WebConsole Ready Event Sequence](AddToHead.svg) 048 * 049 * See {@link ResourceRequest} for details about the processing 050 * of the {@link PageResourceRequest}. 051 * 052 * The `GET` request may also, of course, refer to a resource from 053 * another server and thus not result in a {@link PageResourceRequest}. 054 * 055 * Adding a `<script src=...></script>` node to a document's `<head>` 056 * causes the referenced JavaScript to be loaded asynchronously. This 057 * can cause problems if a dynamically added library relies on another 058 * library to be available. Script resources are therefore specified using 059 * the {@link ScriptResource} class, which allows to specify loading 060 * dependencies between resources. The code in the browser delays the 061 * addition of a `<script>` node until all other script resources that 062 * it depends on are loaded. 063 * 064 * Some libraries provided as page resources may already be required by the 065 * JavaScript web console code (especially by the resource manager that handles 066 * the delayed loading). They can therefore not be loaded by this 067 * mechanism, which depends on the web console code. Such page resources 068 * may be "pre-loaded" by adding the appropriate `script` element to the 069 * initial web console page. In order to make the pre-loading known to the 070 * resource manager, the `script` elements must carry an attribute 071 * `data-jgwc-provides` with a comma separated list of JavaScript resource 072 * names provided by loading the script resource. The name(s) must match 073 * the name(s) used in the {@link AddPageResources} request generated 074 * by the {@link PageResourceProvider} for the pre-loaded resource(s). 075 * Here's an example (for a web console using the 076 * {@link FreeMarkerConsoleWeblet} to generate the initial web console page): 077 * ```html 078 * {@code <}script data-jgwc-provides="jquery" 079 * src="${renderSupport.pageResource('jquery/jquery' + minifiedExtension + '.js')}"{@code >} 080 * {@code <}/script{@code >}; 081 * ``` 082 * 083 * @startuml AddToHead.svg 084 * hide footbox 085 * 086 * activate Browser 087 * Browser -> WebConsole: "consoleReady" 088 * deactivate Browser 089 * activate WebConsole 090 * WebConsole -> PageResourceProvider: ConsoleReady 091 * activate PageResourceProvider 092 * PageResourceProvider -> WebConsole: AddPageResources 093 * deactivate PageResourceProvider 094 * WebConsole -> Browser: "addPageResource" 095 * deactivate WebConsole 096 * activate Browser 097 * deactivate WebConsole 098 * Browser -> WebConsole: "GET <page resource URI>" 099 * activate WebConsole 100 * WebConsole -> PageResourceProvider: PageResourceRequest 101 * deactivate WebConsole 102 * activate PageResourceProvider 103 * deactivate PageResourceProvider 104 * @enduml 105 */ 106@SuppressWarnings("PMD.LinguisticNaming") 107public class AddPageResources extends ConsoleCommand { 108 109 private final List<ScriptResource> scriptResources = new ArrayList<>(); 110 private final List<URI> cssUris = new ArrayList<>(); 111 private String cssSource; 112 113 /** 114 * Add the URI of a JavaScript resource that is to be added to the 115 * header section of the web console page. 116 * 117 * @param scriptResource the resource to add 118 * @return the event for easy chaining 119 */ 120 public AddPageResources addScriptResource(ScriptResource scriptResource) { 121 scriptResources.add(scriptResource); 122 return this; 123 } 124 125 /** 126 * Return all script URIs 127 * 128 * @return the result 129 */ 130 public ScriptResource[] scriptResources() { 131 return scriptResources.toArray(new ScriptResource[0]); 132 } 133 134 /** 135 * Add the URI of a CSS resource that is to be added to the 136 * header section of the web console page. 137 * 138 * @param uri the URI 139 * @return the event for easy chaining 140 */ 141 public AddPageResources addCss(URI uri) { 142 cssUris.add(uri); 143 return this; 144 } 145 146 /** 147 * Return all CSS URIs. 148 * 149 * @return the result 150 */ 151 public URI[] cssUris() { 152 return cssUris.toArray(new URI[0]); 153 } 154 155 /** 156 * @return the cssSource 157 */ 158 public String cssSource() { 159 return cssSource; 160 } 161 162 /** 163 * @param cssSource the cssSource to set 164 */ 165 public AddPageResources setCssSource(String cssSource) { 166 this.cssSource = cssSource; 167 return this; 168 } 169 170 @Override 171 public void toJson(Writer writer) throws IOException { 172 JsonArray scripts = JsonArray.create(); 173 for (ScriptResource scriptResource : scriptResources()) { 174 scripts.append(scriptResource.toJsonValue()); 175 } 176 toJson(writer, "addPageResources", Arrays.stream(cssUris()).map( 177 URI::toString).toArray(String[]::new), 178 cssSource(), scripts); 179 } 180 181 /** 182 * Represents a script resource that is to be loaded or evaluated 183 * by the browser. Note that a single instance can either be used 184 * for a URI or inline JavaScript, not for both. 185 */ 186 public static class ScriptResource { 187 private static final String[] EMPTY_ARRAY = new String[0]; 188 189 private URI scriptUri; 190 private String scriptId; 191 private String scriptType; 192 private String scriptSource; 193 private String[] provides = EMPTY_ARRAY; 194 private String[] requires = EMPTY_ARRAY; 195 196 /** 197 * @return the scriptUri to be loaded 198 */ 199 public URI scriptUri() { 200 return scriptUri; 201 } 202 203 /** 204 * Sets the scriptUri to to be loaded, clears the `scriptSource` 205 * attribute. 206 * 207 * @param scriptUri the scriptUri to to be loaded 208 * @return this object for easy chaining 209 */ 210 public ScriptResource setScriptUri(URI scriptUri) { 211 this.scriptUri = scriptUri; 212 return this; 213 } 214 215 /** 216 * Gets the script type (defaults to no type). 217 * 218 * @return the script type 219 */ 220 public String getScriptType() { 221 return scriptType; 222 } 223 224 /** 225 * Sets the script type. 226 * 227 * @param scriptType the new script type 228 * @return the script resource 229 */ 230 public ScriptResource setScriptType(String scriptType) { 231 this.scriptType = scriptType; 232 return this; 233 } 234 235 /** 236 * Gets the script id (defaults to no id). 237 * 238 * @return the script type 239 */ 240 public String getScriptId() { 241 return scriptId; 242 } 243 244 /** 245 * Sets the script id. 246 * 247 * @param scriptId the script id 248 * @return the script resource 249 */ 250 public ScriptResource setScriptId(String scriptId) { 251 this.scriptId = scriptId; 252 return this; 253 } 254 255 /** 256 * @return the script source 257 */ 258 public String scriptSource() { 259 return scriptSource; 260 } 261 262 /** 263 * Sets the script source to evaluate. Clears the 264 * `scriptUri` attribute. 265 * 266 * @param scriptSource the scriptSource to set 267 * @return this object for easy chaining 268 */ 269 public ScriptResource setScriptSource(String scriptSource) { 270 this.scriptSource = scriptSource; 271 scriptUri = null; 272 return this; 273 } 274 275 /** 276 * Loads the script source to evaluate. Clears the 277 * `scriptUri` attribute. Closes the reader. 278 * 279 * @param in the input stream 280 * @return this object for easy chaining 281 * @throws IOException 282 */ 283 @SuppressWarnings({ "PMD.ShortVariable", "PMD.PreserveStackTrace" }) 284 public ScriptResource loadScriptSource(Reader in) throws IOException { 285 try (BufferedReader buffered = new BufferedReader(in)) { 286 this.scriptSource 287 = buffered.lines().collect(Collectors.joining("\r\n")); 288 } catch (UncheckedIOException e) { 289 throw e.getCause(); 290 } 291 scriptUri = null; 292 return this; 293 } 294 295 /** 296 * Returns the list of JavaScript features that this 297 * script resource provides. 298 * 299 * @return the list of features 300 */ 301 public String[] provides() { 302 return Arrays.copyOf(provides, provides.length); 303 } 304 305 /** 306 * Sets the list of JavaScript features that this 307 * script resource provides. For commonly available 308 * JavaScript libraries, it is recommended to use 309 * their home page URL (without the protocol part) as 310 * feature name. 311 * 312 * @param provides the list of features 313 * @return this object for easy chaining 314 */ 315 public ScriptResource setProvides(String... provides) { 316 this.provides = Arrays.copyOf(provides, provides.length); 317 return this; 318 } 319 320 /** 321 * Returns the list of JavaScript features that this 322 * script resource requires. 323 * 324 * @return the list of features 325 */ 326 public String[] requires() { 327 return Arrays.copyOf(requires, requires.length); 328 } 329 330 /** 331 * Sets the list of JavaScript features that this 332 * script resource requires. 333 * 334 * @param requires the list of features 335 * @return this object for easy chaining 336 */ 337 public ScriptResource setRequires(String... requires) { 338 this.requires = Arrays.copyOf(requires, requires.length); 339 return this; 340 } 341 342 /** 343 * Provides the JSON representation of the information. 344 * 345 * @return the json object 346 */ 347 public JsonObject toJsonValue() { 348 JsonObject obj = JsonObject.create(); 349 if (scriptUri != null) { 350 obj.setField("uri", scriptUri.toString()); 351 } 352 if (scriptId != null) { 353 obj.setField("id", scriptId); 354 } 355 if (scriptType != null) { 356 obj.setField("type", scriptType); 357 } 358 if (scriptSource != null) { 359 obj.setField("source", scriptSource); 360 } 361 JsonArray strArray = JsonArray.create(); 362 for (String req : requires) { 363 strArray.append(req); 364 } 365 obj.setField("requires", strArray); 366 strArray = JsonArray.create(); 367 for (String prov : provides) { 368 strArray.append(prov); 369 } 370 obj.setField("provides", strArray); 371 return obj; 372 } 373 } 374}