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.jdrupes.json.JsonArray; 031import org.jgrapes.core.Channel; 032import org.jgrapes.core.annotation.Handler; 033import org.jgrapes.http.ResourcePattern; 034import org.jgrapes.http.ResponseCreationSupport; 035import org.jgrapes.http.Session; 036import org.jgrapes.http.events.Request; 037import org.jgrapes.http.events.Response; 038import org.jgrapes.io.IOSubchannel; 039import org.jgrapes.util.events.KeyValueStoreUpdate; 040import org.jgrapes.webconsole.base.ConsoleConnection; 041import org.jgrapes.webconsole.base.ConsoleUser; 042import org.jgrapes.webconsole.base.ResourceNotFoundException; 043import org.jgrapes.webconsole.base.WebConsole; 044import org.jgrapes.webconsole.base.WebConsoleUtils; 045import org.jgrapes.webconsole.base.events.JsonInput; 046import org.jgrapes.webconsole.base.events.SimpleConsoleCommand; 047import org.jgrapes.webconsole.base.freemarker.FreeMarkerConsoleWeblet; 048import org.jgrapes.webconsole.jqueryui.events.SetTheme; 049import org.jgrapes.webconsole.jqueryui.themes.base.Provider; 050 051/** 052 * Provides resources using {@link Request}/{@link Response} 053 * events. Some resource requests (page resource, conlet resource) 054 * are forwarded via the {@link WebConsole} component to the 055 * web console components. 056 */ 057@SuppressWarnings({ "PMD.ExcessiveImports", "PMD.NcssCount", 058 "PMD.TooManyMethods", "PMD.LinguisticNaming" }) 059public class JQueryUiWeblet extends FreeMarkerConsoleWeblet { 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 consoleChannel the web console channel 073 * @param consolePrefix the web console prefix 074 */ 075 public JQueryUiWeblet(Channel webletChannel, Channel consoleChannel, 076 URI consolePrefix) { 077 super(webletChannel, consoleChannel, consolePrefix); 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> createConsoleBaseModel() { 101 Map<String, Object> model = super.createConsoleBaseModel(); 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 web console 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 renderConsole(Request.In.Get event, IOSubchannel channel, 124 UUID consoleConnectionId) 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.renderConsole(event, channel, consoleConnectionId); 130 } 131 132 @Override 133 @SuppressWarnings("PMD.AvoidLiteralsInIfCondition") 134 protected void provideConsoleResource(Request.In.Get event, 135 String requestPath, IOSubchannel channel) { 136 String[] requestParts = ResourcePattern.split(requestPath, 1); 137 if (requestParts.length == 2 && "theme".equals(requestParts[0])) { 138 sendThemeResource(event, channel, requestParts[1]); 139 return; 140 } 141 super.provideConsoleResource(event, requestPath, channel); 142 } 143 144 @SuppressWarnings("PMD.DataflowAnomalyAnalysis") 145 private void sendThemeResource(Request.In.Get event, IOSubchannel channel, 146 String resource) { 147 // Get resource 148 ThemeProvider themeProvider 149 = Optional.ofNullable(Session.from(event).get("themeProvider")) 150 .flatMap(themeId -> StreamSupport 151 .stream(themeLoader().spliterator(), false) 152 .filter(thi -> thi.themeId().equals(themeId)) 153 .findFirst()) 154 .orElse(baseTheme); 155 // Get resource 156 URL resourceUrl; 157 try { 158 resourceUrl = themeProvider.getResource(resource); 159 } catch (ResourceNotFoundException e) { 160 try { 161 resourceUrl = baseTheme.getResource(resource); 162 } catch (ResourceNotFoundException e1) { 163 resourceUrl 164 = fallbackResourceSupplier.apply(themeProvider, resource); 165 if (resourceUrl == null) { 166 return; 167 } 168 } 169 } 170 final URL resUrl = resourceUrl; 171 ResponseCreationSupport.sendStaticContent(event, channel, 172 path -> resUrl, null); 173 } 174 175 /** 176 * Handle JSON input. 177 * 178 * @param event the event 179 * @param channel the channel 180 * @throws InterruptedException the interrupted exception 181 * @throws IOException Signals that an I/O exception has occurred. 182 */ 183 @SuppressWarnings("PMD.DataflowAnomalyAnalysis") 184 @Handler(channels = ConsoleChannel.class) 185 public void onJsonInput(JsonInput event, ConsoleConnection channel) 186 throws InterruptedException, IOException { 187 // Send events to conlets on web console's channel 188 JsonArray params = event.request().params(); 189 switch (event.request().method()) { // NOPMD 190 case "setTheme": { 191 fire(new SetTheme(params.asString(0)), channel); 192 break; 193 } 194 default: 195 // Ignore unknown 196 break; 197 } 198 } 199 200 /** 201 * Handles a change of theme. 202 * 203 * @param event the event 204 * @param channel the channel 205 * @throws InterruptedException the interrupted exception 206 * @throws IOException Signals that an I/O exception has occurred. 207 */ 208 @Handler(channels = ConsoleChannel.class) 209 public void onSetTheme(SetTheme event, ConsoleConnection channel) 210 throws InterruptedException, IOException { 211 ThemeProvider themeProvider = StreamSupport 212 .stream(themeLoader().spliterator(), false) 213 .filter(thi -> thi.themeId().equals(event.theme())).findFirst() 214 .orElse(baseTheme); 215 channel.session().put("themeProvider", themeProvider.themeId()); 216 channel.respond(new KeyValueStoreUpdate().update( 217 "/" + WebConsoleUtils.userFromSession(channel.session()) 218 .map(ConsoleUser::getName).orElse("") 219 + "/themeProvider", 220 themeProvider.themeId())).get(); 221 channel.respond(new SimpleConsoleCommand("reload")); 222 } 223 224 /** 225 * Holds the information about a theme. 226 */ 227 public static class ThemeInfo implements Comparable<ThemeInfo> { 228 private final String id; 229 private final String name; 230 231 /** 232 * Instantiates a new theme info. 233 * 234 * @param id the id 235 * @param name the name 236 */ 237 public ThemeInfo(String id, String name) { 238 super(); 239 this.id = id; 240 this.name = name; 241 } 242 243 /** 244 * Returns the id. 245 * 246 * @return the id 247 */ 248 @SuppressWarnings("PMD.ShortMethodName") 249 public String id() { 250 return id; 251 } 252 253 /** 254 * Returns the name. 255 * 256 * @return the name 257 */ 258 public String name() { 259 return name; 260 } 261 262 /* 263 * (non-Javadoc) 264 * 265 * @see java.lang.Comparable#compareTo(java.lang.Object) 266 */ 267 @Override 268 public int compareTo(ThemeInfo other) { 269 return name().compareToIgnoreCase(other.name()); 270 } 271 } 272 273// /** 274// * Create a {@link URI} from a path. This is similar to calling 275// * `new URI(null, null, path, null)` with the {@link URISyntaxException} 276// * converted to a {@link IllegalArgumentException}. 277// * 278// * @param path the path 279// * @return the uri 280// * @throws IllegalArgumentException if the string violates 281// * RFC 2396 282// */ 283// public static URI uriFromPath(String path) throws IllegalArgumentException { 284// try { 285// return new URI(null, null, path, null); 286// } catch (URISyntaxException e) { 287// throw new IllegalArgumentException(e); 288// } 289// } 290// 291// /** 292// * The channel used to send {@link PageResourceRequest}s and 293// * {@link ConletResourceRequest}s to the conlets (via the 294// * web console). 295// */ 296// public class ConsoleResourceChannel extends LinkedIOSubchannel { 297// 298// /** 299// * Instantiates a new web console resource channel. 300// * 301// * @param hub the hub 302// * @param upstreamChannel the upstream channel 303// * @param responsePipeline the response pipeline 304// */ 305// public ConsoleResourceChannel(Manager hub, 306// IOSubchannel upstreamChannel, EventPipeline responsePipeline) { 307// super(hub, hub.channel(), upstreamChannel, responsePipeline); 308// } 309// } 310// 311// /** 312// * The implementation of {@link RenderSupport} used by this class. 313// */ 314// private class RenderSupportImpl implements RenderSupport { 315// 316// @Override 317// public URI conletResource(String conletType, URI uri) { 318// return console.prefix().resolve(uriFromPath( 319// "conlet-resource/" + conletType + "/")).resolve(uri); 320// } 321// 322// @Override 323// public URI pageResource(URI uri) { 324// return console.prefix().resolve(uriFromPath( 325// "page-resource/")).resolve(uri); 326// } 327// 328// @Override 329// public boolean useMinifiedResources() { 330// return useMinifiedResources; 331// } 332// 333// } 334// 335}