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