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.jqueryui;
020
021import java.io.IOException;
022import java.net.URI;
023import java.net.URL;
024import java.util.Map;
025import java.util.Optional;
026import java.util.ServiceLoader;
027import java.util.UUID;
028import java.util.function.BiFunction;
029import java.util.stream.StreamSupport;
030import org.jdrupes.json.JsonArray;
031import org.jgrapes.core.Channel;
032import org.jgrapes.core.annotation.Handler;
033import org.jgrapes.http.ResourcePattern;
034import org.jgrapes.http.ResponseCreationSupport;
035import org.jgrapes.http.Session;
036import org.jgrapes.http.events.Request;
037import org.jgrapes.http.events.Response;
038import org.jgrapes.io.IOSubchannel;
039import org.jgrapes.util.events.KeyValueStoreUpdate;
040import org.jgrapes.webconsole.base.ConsoleConnection;
041import org.jgrapes.webconsole.base.ConsoleUser;
042import org.jgrapes.webconsole.base.ResourceNotFoundException;
043import org.jgrapes.webconsole.base.WebConsole;
044import org.jgrapes.webconsole.base.WebConsoleUtils;
045import org.jgrapes.webconsole.base.events.JsonInput;
046import org.jgrapes.webconsole.base.events.SimpleConsoleCommand;
047import org.jgrapes.webconsole.base.freemarker.FreeMarkerConsoleWeblet;
048import org.jgrapes.webconsole.jqueryui.events.SetTheme;
049import org.jgrapes.webconsole.jqueryui.themes.base.Provider;
050
051/**
052 * Provides resources using {@link Request}/{@link Response}
053 * events. Some resource requests (page resource, conlet resource)
054 * are forwarded via the {@link WebConsole} component to the 
055 * web console components.
056 */
057@SuppressWarnings({ "PMD.ExcessiveImports", "PMD.NcssCount",
058    "PMD.TooManyMethods", "PMD.LinguisticNaming" })
059public class JQueryUiWeblet extends FreeMarkerConsoleWeblet {
060
061    private ServiceLoader<ThemeProvider> themeLoader;
062    private final ThemeProvider baseTheme;
063    private BiFunction<ThemeProvider, String, URL> fallbackResourceSupplier
064        = (themeProvider, resource) -> {
065            return null;
066        };
067
068    /**
069     * Instantiates a new jQuery UI weblet.
070     *
071     * @param webletChannel the weblet channel
072     * @param consoleChannel the web console channel
073     * @param consolePrefix the web console prefix
074     */
075    public JQueryUiWeblet(Channel webletChannel, Channel consoleChannel,
076            URI consolePrefix) {
077        super(webletChannel, consoleChannel, consolePrefix);
078        baseTheme = new Provider();
079    }
080
081    @Override
082    public String styling() {
083        return "jqueryui";
084    }
085
086    /**
087     * The service loader must be created lazily, else the OSGi
088     * service mediator doesn't work properly.
089     * 
090     * @return
091     */
092    private ServiceLoader<ThemeProvider> themeLoader() {
093        if (themeLoader != null) {
094            return themeLoader;
095        }
096        return themeLoader = ServiceLoader.load(ThemeProvider.class);
097    }
098
099    @Override
100    protected Map<String, Object> createConsoleBaseModel() {
101        Map<String, Object> model = super.createConsoleBaseModel();
102        model.put("themeInfos",
103            StreamSupport.stream(themeLoader().spliterator(), false)
104                .map(thi -> new ThemeInfo(thi.themeId(), thi.themeName()))
105                .sorted().toArray(size -> new ThemeInfo[size]));
106        return model;
107    }
108
109    /**
110     * Sets a function for obtaining a fallback resource bundle for
111     * a given theme provider and locale.
112     * 
113     * @param supplier the function
114     * @return the web console fo reasy chaining
115     */
116    public JQueryUiWeblet setFallbackResourceSupplier(
117            BiFunction<ThemeProvider, String, URL> supplier) {
118        fallbackResourceSupplier = supplier;
119        return this;
120    }
121
122    @Override
123    protected void renderConsole(Request.In.Get event, IOSubchannel channel,
124            UUID consoleConnectionId) throws IOException, InterruptedException {
125        // Reloading themes on every reload allows themes
126        // to be added dynamically. Note that we must load again
127        // (not reload) in order for this to work in an OSGi environment.
128        themeLoader = null;
129        super.renderConsole(event, channel, consoleConnectionId);
130    }
131
132    @Override
133    @SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
134    protected void provideConsoleResource(Request.In.Get event,
135            String requestPath, IOSubchannel channel) {
136        String[] requestParts = ResourcePattern.split(requestPath, 1);
137        if (requestParts.length == 2 && "theme".equals(requestParts[0])) {
138            sendThemeResource(event, channel, requestParts[1]);
139            return;
140        }
141        super.provideConsoleResource(event, requestPath, channel);
142    }
143
144    @SuppressWarnings("PMD.DataflowAnomalyAnalysis")
145    private void sendThemeResource(Request.In.Get event, IOSubchannel channel,
146            String resource) {
147        // Get resource
148        ThemeProvider themeProvider
149            = Optional.ofNullable(Session.from(event).get("themeProvider"))
150                .flatMap(themeId -> StreamSupport
151                    .stream(themeLoader().spliterator(), false)
152                    .filter(thi -> thi.themeId().equals(themeId))
153                    .findFirst())
154                .orElse(baseTheme);
155        // Get resource
156        URL resourceUrl;
157        try {
158            resourceUrl = themeProvider.getResource(resource);
159        } catch (ResourceNotFoundException e) {
160            try {
161                resourceUrl = baseTheme.getResource(resource);
162            } catch (ResourceNotFoundException e1) {
163                resourceUrl
164                    = fallbackResourceSupplier.apply(themeProvider, resource);
165                if (resourceUrl == null) {
166                    return;
167                }
168            }
169        }
170        final URL resUrl = resourceUrl;
171        ResponseCreationSupport.sendStaticContent(event, channel,
172            path -> resUrl, null);
173    }
174
175    /**
176     * Handle JSON input.
177     *
178     * @param event the event
179     * @param channel the channel
180     * @throws InterruptedException the interrupted exception
181     * @throws IOException Signals that an I/O exception has occurred.
182     */
183    @SuppressWarnings("PMD.DataflowAnomalyAnalysis")
184    @Handler(channels = ConsoleChannel.class)
185    public void onJsonInput(JsonInput event, ConsoleConnection channel)
186            throws InterruptedException, IOException {
187        // Send events to conlets on web console's channel
188        JsonArray params = event.request().params();
189        switch (event.request().method()) { // NOPMD
190        case "setTheme": {
191            fire(new SetTheme(params.asString(0)), channel);
192            break;
193        }
194        default:
195            // Ignore unknown
196            break;
197        }
198    }
199
200    /**
201     * Handles a change of theme.
202     *
203     * @param event the event
204     * @param channel the channel
205     * @throws InterruptedException the interrupted exception
206     * @throws IOException Signals that an I/O exception has occurred.
207     */
208    @Handler(channels = ConsoleChannel.class)
209    public void onSetTheme(SetTheme event, ConsoleConnection channel)
210            throws InterruptedException, IOException {
211        ThemeProvider themeProvider = StreamSupport
212            .stream(themeLoader().spliterator(), false)
213            .filter(thi -> thi.themeId().equals(event.theme())).findFirst()
214            .orElse(baseTheme);
215        channel.session().put("themeProvider", themeProvider.themeId());
216        channel.respond(new KeyValueStoreUpdate().update(
217            "/" + WebConsoleUtils.userFromSession(channel.session())
218                .map(ConsoleUser::getName).orElse("")
219                + "/themeProvider",
220            themeProvider.themeId())).get();
221        channel.respond(new SimpleConsoleCommand("reload"));
222    }
223
224    /**
225     * Holds the information about a theme.
226     */
227    public static class ThemeInfo implements Comparable<ThemeInfo> {
228        private final String id;
229        private final String name;
230
231        /**
232         * Instantiates a new theme info.
233         *
234         * @param id the id
235         * @param name the name
236         */
237        public ThemeInfo(String id, String name) {
238            super();
239            this.id = id;
240            this.name = name;
241        }
242
243        /**
244         * Returns the id.
245         *
246         * @return the id
247         */
248        @SuppressWarnings("PMD.ShortMethodName")
249        public String id() {
250            return id;
251        }
252
253        /**
254         * Returns the name.
255         *
256         * @return the name
257         */
258        public String name() {
259            return name;
260        }
261
262        /*
263         * (non-Javadoc)
264         * 
265         * @see java.lang.Comparable#compareTo(java.lang.Object)
266         */
267        @Override
268        public int compareTo(ThemeInfo other) {
269            return name().compareToIgnoreCase(other.name());
270        }
271    }
272
273//    /**
274//     * Create a {@link URI} from a path. This is similar to calling
275//     * `new URI(null, null, path, null)` with the {@link URISyntaxException}
276//     * converted to a {@link IllegalArgumentException}.
277//     * 
278//     * @param path the path
279//     * @return the uri
280//     * @throws IllegalArgumentException if the string violates 
281//     * RFC 2396
282//     */
283//    public static URI uriFromPath(String path) throws IllegalArgumentException {
284//        try {
285//            return new URI(null, null, path, null);
286//        } catch (URISyntaxException e) {
287//            throw new IllegalArgumentException(e);
288//        }
289//    }
290//
291//    /**
292//     * The channel used to send {@link PageResourceRequest}s and
293//     * {@link ConletResourceRequest}s to the conlets (via the
294//     * web console).
295//     */
296//    public class ConsoleResourceChannel extends LinkedIOSubchannel {
297//
298//        /**
299//         * Instantiates a new web console resource channel.
300//         *
301//         * @param hub the hub
302//         * @param upstreamChannel the upstream channel
303//         * @param responsePipeline the response pipeline
304//         */
305//        public ConsoleResourceChannel(Manager hub,
306//                IOSubchannel upstreamChannel, EventPipeline responsePipeline) {
307//            super(hub, hub.channel(), upstreamChannel, responsePipeline);
308//        }
309//    }
310//
311//    /**
312//     * The implementation of {@link RenderSupport} used by this class.
313//     */
314//    private class RenderSupportImpl implements RenderSupport {
315//
316//        @Override
317//        public URI conletResource(String conletType, URI uri) {
318//            return console.prefix().resolve(uriFromPath(
319//                "conlet-resource/" + conletType + "/")).resolve(uri);
320//        }
321//
322//        @Override
323//        public URI pageResource(URI uri) {
324//            return console.prefix().resolve(uriFromPath(
325//                "page-resource/")).resolve(uri);
326//        }
327//
328//        @Override
329//        public boolean useMinifiedResources() {
330//            return useMinifiedResources;
331//        }
332//
333//    }
334//
335}