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