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 `<link .../>`, `<style>...</style>` or `<script ...></script>` nodes
038 * to the web console's `<head>` node on behalf of components that provide
039 * 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 */
106public class AddPageResources extends ConsoleCommand {
107
108    private final List<ScriptResource> scriptResources = new ArrayList<>();
109    private final List<URI> cssUris = new ArrayList<>();
110    private String cssSource;
111
112    /**
113     * Add the URI of a JavaScript resource that is to be added to the
114     * header section of the web console page.
115     * 
116     * @param scriptResource the resource to add
117     * @return the event for easy chaining
118     */
119    public AddPageResources addScriptResource(ScriptResource scriptResource) {
120        scriptResources.add(scriptResource);
121        return this;
122    }
123
124    /**
125     * Return all script URIs
126     * 
127     * @return the result
128     */
129    public ScriptResource[] scriptResources() {
130        return scriptResources.toArray(new ScriptResource[0]);
131    }
132
133    /**
134     * Add the URI of a CSS resource that is to be added to the
135     * header section of the web console page.
136     * 
137     * @param uri the URI
138     * @return the event for easy chaining
139     */
140    public AddPageResources addCss(URI uri) {
141        cssUris.add(uri);
142        return this;
143    }
144
145    /**
146     * Return all CSS URIs.
147     * 
148     * @return the result
149     */
150    public URI[] cssUris() {
151        return cssUris.toArray(new URI[0]);
152    }
153
154    /**
155     * @return the cssSource
156     */
157    public String cssSource() {
158        return cssSource;
159    }
160
161    /**
162     * @param cssSource the cssSource to set
163     */
164    public AddPageResources setCssSource(String cssSource) {
165        this.cssSource = cssSource;
166        return this;
167    }
168
169    @Override
170    public void toJson(Writer writer) throws IOException {
171        JsonArray scripts = JsonArray.create();
172        for (ScriptResource scriptResource : scriptResources()) {
173            scripts.append(scriptResource.toJsonValue());
174        }
175        toJson(writer, "addPageResources", Arrays.stream(cssUris()).map(
176            uri -> uri.toString()).toArray(String[]::new),
177            cssSource(), scripts);
178    }
179
180    /**
181     * Represents a script resource that is to be loaded or evaluated
182     * by the browser. Note that a single instance can either be used
183     * for a URI or inline JavaScript, not for both.
184     */
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        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         * Provides the JSON representation of the information.
342         *
343         * @return the json object
344         */
345        public JsonObject toJsonValue() {
346            JsonObject obj = JsonObject.create();
347            if (scriptUri != null) {
348                obj.setField("uri", scriptUri.toString());
349            }
350            if (scriptId != null) {
351                obj.setField("id", scriptId);
352            }
353            if (scriptType != null) {
354                obj.setField("type", scriptType);
355            }
356            if (scriptSource != null) {
357                obj.setField("source", scriptSource);
358            }
359            JsonArray strArray = JsonArray.create();
360            for (String req : requires) {
361                strArray.append(req);
362            }
363            obj.setField("requires", strArray);
364            strArray = JsonArray.create();
365            for (String prov : provides) {
366                strArray.append(prov);
367            }
368            obj.setField("provides", strArray);
369            return obj;
370        }
371    }
372}