001/* 002 * JGrapes Event Driven Framework 003 * Copyright (C) 2026 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.helloalpine; 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.nio.charset.StandardCharsets; 029import java.util.EnumSet; 030import java.util.Optional; 031import java.util.Set; 032import org.jgrapes.core.Channel; 033import org.jgrapes.core.Event; 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 * Hello World example using AlpineJS for reactive UI. 057 */ 058public class HelloAlpineConlet 059 extends FreeMarkerConlet<HelloAlpineConlet.HelloAlpineModel> { 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 * Instantiates a new hello alpine conlet. 070 * 071 * @param componentChannel the component channel 072 */ 073 public HelloAlpineConlet(Channel componentChannel) { 074 super(componentChannel); 075 } 076 077 private String storagePath(Session session, String conletId) { 078 return "/" + WebConsoleUtils.userFromSession(session) 079 .map(ConsoleUser::getName).orElse("") 080 + "/" + HelloAlpineConlet.class.getName() + "/" + conletId; 081 } 082 083 /** 084 * Called when the console is ready and the conlet can be registered. 085 * 086 * @param event the event 087 * @param connection the connection 088 * @throws TemplateNotFoundException the template not found exception 089 * @throws MalformedTemplateNameException the malformed template name exception 090 * @throws ParseException the parse exception 091 * @throws IOException Signals that an I/O exception has occurred. 092 */ 093 @Handler 094 public void onConsoleReady(ConsoleReady event, ConsoleConnection connection) 095 throws TemplateNotFoundException, MalformedTemplateNameException, 096 ParseException, IOException { 097 connection.respond(new AddConletType(type()) 098 .addRenderMode(RenderMode.Preview).setDisplayNames( 099 localizations(connection.supportedLocales(), "conletName")) 100 .addScript(new ScriptResource() 101 .setScriptType("module").setScriptUri( 102 event.renderSupport().conletResource(type(), 103 "HelloAlpine-functions.js"))) 104 .addCss(event.renderSupport(), WebConsoleUtils.uriFromPath( 105 "HelloAlpine-style.css"))); 106 } 107 108 @Override 109 protected Optional<HelloAlpineModel> createStateRepresentation( 110 Event<?> event, ConsoleConnection channel, String conletId) 111 throws IOException { 112 HelloAlpineModel conletModel = new HelloAlpineModel(conletId); 113 String jsonState = mapper.writer().writeValueAsString(conletModel); 114 channel.respond(new KeyValueStoreUpdate().update( 115 storagePath(channel.session(), conletModel.getConletId()), 116 jsonState)); 117 return Optional.of(conletModel); 118 } 119 120 @Override 121 @SuppressWarnings("PMD.EmptyCatchBlock") 122 protected Optional<HelloAlpineModel> recreateState(Event<?> event, 123 ConsoleConnection channel, String conletId) throws Exception { 124 KeyValueStoreQuery query = new KeyValueStoreQuery( 125 storagePath(channel.session(), conletId), channel); 126 newEventPipeline().fire(query, channel); 127 try { 128 if (!query.results().isEmpty()) { 129 var json = query.results().get(0).values().stream().findFirst() 130 .get(); 131 HelloAlpineModel model = mapper.readValue( 132 json.getBytes(StandardCharsets.UTF_8), 133 HelloAlpineModel.class); 134 return Optional.of(model); 135 } 136 } catch (InterruptedException | IOException e) { 137 // Means we have no result. 138 } 139 return createStateRepresentation(event, channel, conletId); 140 } 141 142 @Override 143 protected Set<RenderMode> doRenderConlet(RenderConletRequestBase<?> event, 144 ConsoleConnection channel, String conletId, 145 HelloAlpineModel conletState) throws Exception { 146 Set<RenderMode> renderedAs = EnumSet.noneOf(RenderMode.class); 147 if (event.renderAs().contains(RenderMode.Preview)) { 148 Template tpl 149 = freemarkerConfig() 150 .getTemplate("HelloAlpine-preview.ftl.html"); 151 channel.respond(new RenderConlet(type(), conletId, 152 processTemplate(event, tpl, 153 fmModel(event, channel, conletId, conletState))) 154 .setRenderAs( 155 RenderMode.Preview.addModifiers(event.renderAs())) 156 .setSupportedModes(MODES)); 157 renderedAs.add(RenderMode.Preview); 158 } 159 if (event.renderAs().contains(RenderMode.View)) { 160 Template tpl 161 = freemarkerConfig().getTemplate("HelloAlpine-view.ftl.html"); 162 channel.respond(new RenderConlet(type(), conletState.getConletId(), 163 processTemplate(event, tpl, 164 fmModel(event, channel, conletId, conletState))) 165 .setRenderAs( 166 RenderMode.View.addModifiers(event.renderAs())) 167 .setSupportedModes(MODES)); 168 channel.respond(new NotifyConletView(type(), 169 conletState.getConletId(), "setWorldVisible", 170 conletState.isWorldVisible())); 171 renderedAs.add(RenderMode.View); 172 } 173 if (event.renderAs().contains(RenderMode.Help)) { 174 Template tpl = freemarkerConfig() 175 .getTemplate("HelloAlpine-help.ftl.html"); 176 channel.respond(new OpenModalDialog(type(), conletId, 177 processTemplate(event, tpl, 178 fmModel(event, channel, conletId, conletState))) 179 .addOption("cancelable", true) 180 .addOption("closeLabel", "")); 181 } 182 return renderedAs; 183 } 184 185 @Override 186 protected void doConletDeleted(ConletDeleted event, 187 ConsoleConnection channel, String conletId, 188 HelloAlpineModel conletState) throws Exception { 189 if (event.renderModes().isEmpty()) { 190 channel.respond(new KeyValueStoreUpdate().delete( 191 storagePath(channel.session(), conletId))); 192 } 193 } 194 195 @Override 196 protected void doUpdateConletState(NotifyConletModel event, 197 ConsoleConnection channel, HelloAlpineModel conletModel) 198 throws Exception { 199 event.stop(); 200 conletModel.setWorldVisible(!conletModel.isWorldVisible()); 201 202 String jsonState = mapper.writer().writeValueAsString(conletModel); 203 channel.respond(new KeyValueStoreUpdate().update( 204 storagePath(channel.session(), conletModel.getConletId()), 205 jsonState)); 206 channel.respond(new NotifyConletView(type(), 207 conletModel.getConletId(), "setWorldVisible", 208 conletModel.isWorldVisible())); 209 channel.respond(new DisplayNotification("<span>" 210 + resourceBundle(channel.locale()).getString("visibilityChange") 211 + "</span>") 212 .addOption("autoClose", 2000)); 213 } 214 215 /** 216 * Model with world's state. 217 */ 218 public static class HelloAlpineModel extends ConletBaseModel { 219 220 private boolean worldVisible = true; 221 222 /** 223 * Instantiates a new hello alpine model. 224 * 225 * @param conletId the conlet id 226 */ 227 @ConstructorProperties({ "conletId" }) 228 public HelloAlpineModel(String conletId) { 229 super(conletId); 230 } 231 232 public void setWorldVisible(boolean visible) { 233 this.worldVisible = visible; 234 } 235 236 public boolean isWorldVisible() { 237 return worldVisible; 238 } 239 } 240}