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 &#x2e;&#x2e;&#x2e;></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 -> 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")
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}