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}