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