001/* 002 * JGrapes Event Driven Framework 003 * Copyright (C) 2025 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.webconlet.examples.hellosolid; 020 021import com.fasterxml.jackson.databind.ObjectMapper; 022import freemarker.core.ParseException; 023import freemarker.template.MalformedTemplateNameException; 024import freemarker.template.Template; 025import freemarker.template.TemplateNotFoundException; 026import java.beans.ConstructorProperties; 027import java.io.IOException; 028import java.util.HashSet; 029import java.util.Optional; 030import java.util.Set; 031import org.jgrapes.core.Channel; 032import org.jgrapes.core.Event; 033import org.jgrapes.core.Manager; 034import org.jgrapes.core.annotation.Handler; 035import org.jgrapes.http.Session; 036import org.jgrapes.util.events.KeyValueStoreQuery; 037import org.jgrapes.util.events.KeyValueStoreUpdate; 038import org.jgrapes.webconsole.base.Conlet.RenderMode; 039import org.jgrapes.webconsole.base.ConletBaseModel; 040import org.jgrapes.webconsole.base.ConsoleConnection; 041import org.jgrapes.webconsole.base.ConsoleUser; 042import org.jgrapes.webconsole.base.WebConsoleUtils; 043import org.jgrapes.webconsole.base.events.AddConletType; 044import org.jgrapes.webconsole.base.events.AddPageResources.ScriptResource; 045import org.jgrapes.webconsole.base.events.ConletDeleted; 046import org.jgrapes.webconsole.base.events.ConsoleReady; 047import org.jgrapes.webconsole.base.events.DisplayNotification; 048import org.jgrapes.webconsole.base.events.NotifyConletModel; 049import org.jgrapes.webconsole.base.events.NotifyConletView; 050import org.jgrapes.webconsole.base.events.OpenModalDialog; 051import org.jgrapes.webconsole.base.events.RenderConlet; 052import org.jgrapes.webconsole.base.events.RenderConletRequestBase; 053import org.jgrapes.webconsole.base.freemarker.FreeMarkerConlet; 054 055/** 056 * Example of a simple conlet. 057 */ 058public class HelloSolidConlet 059 extends FreeMarkerConlet<HelloSolidConlet.HelloSolidModel> { 060 061 /** The mapper. */ 062 @SuppressWarnings("PMD.FieldNamingConventions") 063 protected static final ObjectMapper mapper = new ObjectMapper(); 064 065 private static final Set<RenderMode> MODES = RenderMode.asSet( 066 RenderMode.Preview, RenderMode.View, RenderMode.Help); 067 068 /** 069 * Creates a new component with its channel set to the given 070 * channel. 071 * 072 * @param componentChannel the channel that the component's 073 * handlers listen on by default and that 074 * {@link Manager#fire(Event, Channel...)} sends the event to 075 */ 076 public HelloSolidConlet(Channel componentChannel) { 077 super(componentChannel); 078 } 079 080 private String storagePath(Session session, String conletId) { 081 return "/" + WebConsoleUtils.userFromSession(session) 082 .map(ConsoleUser::getName).orElse("") 083 + "/" + HelloSolidConlet.class.getName() + "/" + conletId; 084 } 085 086 /** 087 * Trigger loading of resources when the console is ready. 088 * 089 * @param event the event 090 * @param connection the console connection 091 * @throws TemplateNotFoundException the template not found exception 092 * @throws MalformedTemplateNameException the malformed template name exception 093 * @throws ParseException the parse exception 094 * @throws IOException Signals that an I/O exception has occurred. 095 */ 096 @Handler 097 public void onConsoleReady(ConsoleReady event, ConsoleConnection connection) 098 throws TemplateNotFoundException, MalformedTemplateNameException, 099 ParseException, IOException { 100 // Add HelloWorldConlet resources to page 101 connection.respond(new AddConletType(type()) 102 .addRenderMode(RenderMode.Preview).setDisplayNames( 103 localizations(connection.supportedLocales(), "conletName")) 104 .addScript(new ScriptResource() 105 .setScriptUri(event.renderSupport().conletResource( 106 type(), "HelloSolid-functions.js")) 107 .setScriptType("module"))); 108// .addScript(new ScriptResource() 109// .setScriptType("module").setScriptUri( 110// event.renderSupport().conletResource(type(), 111// "HelloSolid-functions.js"))) 112// .addCss(event.renderSupport(), WebConsoleUtils.uriFromPath( 113// "HelloSolid-style.css"))); 114 } 115 116 @Override 117 protected Optional<HelloSolidModel> createStateRepresentation( 118 Event<?> event, ConsoleConnection channel, String conletId) 119 throws IOException { 120 HelloSolidModel conletModel = new HelloSolidModel(conletId); 121 String jsonState = mapper.writer().writeValueAsString(conletModel); 122 channel.respond(new KeyValueStoreUpdate().update( 123 storagePath(channel.session(), conletModel.getConletId()), 124 jsonState)); 125 return Optional.of(conletModel); 126 } 127 128 @Override 129 @SuppressWarnings("PMD.EmptyCatchBlock") 130 protected Optional<HelloSolidModel> recreateState(Event<?> event, 131 ConsoleConnection channel, String conletId) throws Exception { 132 KeyValueStoreQuery query = new KeyValueStoreQuery( 133 storagePath(channel.session(), conletId), channel); 134 newEventPipeline().fire(query, channel); 135 try { 136 if (!query.results().isEmpty()) { 137 var json = query.results().get(0).values().stream().findFirst() 138 .get(); 139 HelloSolidModel model 140 = mapper.readValue(json.getBytes(), HelloSolidModel.class); 141 return Optional.of(model); 142 } 143 } catch (InterruptedException | IOException e) { 144 // Means we have no result. 145 } 146 147 // Fall back to creating default state. 148 return createStateRepresentation(event, channel, conletId); 149 } 150 151 @Override 152 protected Set<RenderMode> doRenderConlet(RenderConletRequestBase<?> event, 153 ConsoleConnection channel, String conletId, 154 HelloSolidModel conletState) throws Exception { 155 Set<RenderMode> renderedAs = new HashSet<>(); 156 if (event.renderAs().contains(RenderMode.Preview)) { 157 Template tpl 158 = freemarkerConfig().getTemplate("HelloSolid-preview.ftl.html"); 159 channel.respond(new RenderConlet(type(), conletId, 160 processTemplate(event, tpl, 161 fmModel(event, channel, conletId, conletState))) 162 .setRenderAs( 163 RenderMode.Preview.addModifiers(event.renderAs())) 164 .setSupportedModes(MODES)); 165 renderedAs.add(RenderMode.Preview); 166 } 167 if (event.renderAs().contains(RenderMode.View)) { 168 Template tpl 169 = freemarkerConfig().getTemplate("HelloSolid-view.ftl.html"); 170 channel.respond(new RenderConlet(type(), conletState.getConletId(), 171 processTemplate(event, tpl, 172 fmModel(event, channel, conletId, conletState))) 173 .setRenderAs( 174 RenderMode.View.addModifiers(event.renderAs())) 175 .setSupportedModes(MODES)); 176 channel.respond(new NotifyConletView(type(), 177 conletState.getConletId(), "setWorldVisible", 178 conletState.isWorldVisible())); 179 renderedAs.add(RenderMode.View); 180 } 181 if (event.renderAs().contains(RenderMode.Help)) { 182 Template tpl = freemarkerConfig() 183 .getTemplate("HelloSolid-help.ftl.html"); 184 channel.respond(new OpenModalDialog(type(), conletId, 185 processTemplate(event, tpl, 186 fmModel(event, channel, conletId, conletState))) 187 .addOption("cancelable", true) 188 .addOption("closeLabel", "")); 189 } 190 return renderedAs; 191 } 192 193 @Override 194 protected void doConletDeleted(ConletDeleted event, 195 ConsoleConnection channel, String conletId, 196 HelloSolidModel conletState) throws Exception { 197 if (event.renderModes().isEmpty()) { 198 channel.respond(new KeyValueStoreUpdate().delete( 199 storagePath(channel.session(), conletId))); 200 } 201 } 202 203 @Override 204 protected void doUpdateConletState(NotifyConletModel event, 205 ConsoleConnection channel, HelloSolidModel conletModel) 206 throws Exception { 207 event.stop(); 208 conletModel.setWorldVisible(!conletModel.isWorldVisible()); 209 210 String jsonState = mapper.writer().writeValueAsString(conletModel); 211 channel.respond(new KeyValueStoreUpdate().update( 212 storagePath(channel.session(), conletModel.getConletId()), 213 jsonState)); 214 channel.respond(new NotifyConletView(type(), 215 conletModel.getConletId(), "setWorldVisible", 216 conletModel.isWorldVisible())); 217 channel.respond(new DisplayNotification("<span>" 218 + resourceBundle(channel.locale()).getString("visibilityChange") 219 + "</span>") 220 .addOption("autoClose", 2000)); 221 } 222 223 /** 224 * Model with world's state. 225 */ 226 public static class HelloSolidModel extends ConletBaseModel { 227 228 private boolean worldVisible = true; 229 230 /** 231 * Creates a new model with the given type and id. 232 * 233 * @param conletId the web console component id 234 */ 235 @ConstructorProperties({ "conletId" }) 236 public HelloSolidModel(String conletId) { 237 super(conletId); 238 } 239 240 /** 241 * @param visible the visible to set 242 */ 243 public void setWorldVisible(boolean visible) { 244 this.worldVisible = visible; 245 } 246 247 public boolean isWorldVisible() { 248 return worldVisible; 249 } 250 } 251 252}