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