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}