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 */
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}