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