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.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;
030
031import org.jdrupes.json.JsonArray;
032import org.jgrapes.core.Channel;
033import org.jgrapes.core.annotation.Handler;
034import org.jgrapes.http.ResourcePattern;
035import org.jgrapes.http.ResponseCreationSupport;
036import org.jgrapes.http.Session;
037import org.jgrapes.http.events.Request;
038import org.jgrapes.http.events.Response;
039import org.jgrapes.io.IOSubchannel;
040import org.jgrapes.portal.base.Portal;
041import org.jgrapes.portal.base.PortalSession;
042import org.jgrapes.portal.base.PortalUtils;
043import org.jgrapes.portal.base.ResourceNotFoundException;
044import org.jgrapes.portal.base.UserPrincipal;
045import org.jgrapes.portal.base.events.JsonInput;
046import org.jgrapes.portal.base.events.SimplePortalCommand;
047import org.jgrapes.portal.base.freemarker.FreeMarkerPortalWeblet;
048import org.jgrapes.portal.jqueryui.events.SetTheme;
049import org.jgrapes.portal.jqueryui.themes.base.Provider;
050import org.jgrapes.util.events.KeyValueStoreUpdate;
051
052/**
053 * Provides resources using {@link Request}/{@link Response}
054 * events. Some resource requests (page resource, portlet resource)
055 * are forwarded via the {@link Portal} component to the portlets.
056 */
057@SuppressWarnings({ "PMD.ExcessiveImports", "PMD.NcssCount",
058    "PMD.TooManyMethods" })
059public class JQueryUiWeblet extends FreeMarkerPortalWeblet {
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 portalChannel the portal channel
073     * @param portalPrefix the portal prefix
074     */
075    public JQueryUiWeblet(Channel webletChannel, Channel portalChannel,
076            URI portalPrefix) {
077        super(webletChannel, portalChannel, portalPrefix);
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> createPortalBaseModel() {
101        Map<String, Object> model = super.createPortalBaseModel();
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 portal 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 renderPortal(Request.In.Get event, IOSubchannel channel,
124            UUID portalSessionId) 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.renderPortal(event, channel, portalSessionId);
130    }
131
132    @Override
133    protected void providePortalResource(Request.In.Get event,
134            String requestPath, IOSubchannel channel) {
135        String[] requestParts = ResourcePattern.split(requestPath, 1);
136        if (requestParts.length == 2 && requestParts[0].equals("theme")) {
137            sendThemeResource(event, channel, requestParts[1]);
138            return;
139        }
140        super.providePortalResource(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 = event.associated(Session.class).flatMap(
148            session -> Optional.ofNullable(session.get("themeProvider"))
149                .flatMap(
150                    themeId -> StreamSupport
151                        .stream(themeLoader().spliterator(), false)
152                        .filter(thi -> thi.themeId().equals(themeId))
153                        .findFirst()))
154            .orElse(baseTheme);
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 = PortalChannel.class)
184    public void onJsonInput(JsonInput event, PortalSession channel)
185            throws InterruptedException, IOException {
186        // Send events to portlets on portal's channel
187        JsonArray params = event.request().params();
188        switch (event.request().method()) { // NOPMD
189        case "setTheme": {
190            fire(new SetTheme(params.asString(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 = PortalChannel.class)
208    public void onSetTheme(SetTheme event, PortalSession 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.browserSession().put("themeProvider", themeProvider.themeId());
215        channel.respond(new KeyValueStoreUpdate().update(
216            "/" + PortalUtils.userFromSession(channel.browserSession())
217                .map(UserPrincipal::toString).orElse("")
218                + "/themeProvider",
219            themeProvider.themeId())).get();
220        channel.respond(new SimplePortalCommand("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 PortletResourceRequest}s to the portlets (via the
293//     * portal).
294//     */
295//    public class PortalResourceChannel extends LinkedIOSubchannel {
296//
297//        /**
298//         * Instantiates a new portal resource channel.
299//         *
300//         * @param hub the hub
301//         * @param upstreamChannel the upstream channel
302//         * @param responsePipeline the response pipeline
303//         */
304//        public PortalResourceChannel(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//        /*
316//         * (non-Javadoc)
317//         * 
318//         * @see
319//         * org.jgrapes.portal.RenderSupport#portletResource(java.lang.String,
320//         * java.net.URI)
321//         */
322//        @Override
323//        public URI portletResource(String portletType, URI uri) {
324//            return portal.prefix().resolve(uriFromPath(
325//                "portlet-resource/" + portletType + "/")).resolve(uri);
326//        }
327//
328//        /*
329//         * (non-Javadoc)
330//         * 
331//         * @see org.jgrapes.portal.RenderSupport#pageResource(java.net.URI)
332//         */
333//        @Override
334//        public URI pageResource(URI uri) {
335//            return portal.prefix().resolve(uriFromPath(
336//                "page-resource/")).resolve(uri);
337//        }
338//
339//        /*
340//         * (non-Javadoc)
341//         * 
342//         * @see org.jgrapes.portal.RenderSupport#useMinifiedResources()
343//         */
344//        @Override
345//        public boolean useMinifiedResources() {
346//            return useMinifiedResources;
347//        }
348//
349//    }
350//
351}