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 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 HelloWorldConlet 059 extends FreeMarkerConlet<HelloWorldConlet.HelloWorldModel> { 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 HelloWorldConlet(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 + "/" + HelloWorldConlet.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 .setRequires("jquery").setScriptUri( 106 event.renderSupport().conletResource(type(), 107 "HelloWorld-functions.js"))) 108 .addCss(event.renderSupport(), WebConsoleUtils.uriFromPath( 109 "HelloWorld-style.css"))); 110 } 111 112 @Override 113 protected Optional<HelloWorldModel> createStateRepresentation( 114 Event<?> event, ConsoleConnection channel, String conletId) 115 throws IOException { 116 HelloWorldModel conletModel = new HelloWorldModel(conletId); 117 String jsonState = mapper.writer().writeValueAsString(conletModel); 118 channel.respond(new KeyValueStoreUpdate().update( 119 storagePath(channel.session(), conletModel.getConletId()), 120 jsonState)); 121 return Optional.of(conletModel); 122 } 123 124 @Override 125 @SuppressWarnings("PMD.EmptyCatchBlock") 126 protected Optional<HelloWorldModel> recreateState(Event<?> event, 127 ConsoleConnection channel, String conletId) throws Exception { 128 KeyValueStoreQuery query = new KeyValueStoreQuery( 129 storagePath(channel.session(), conletId), channel); 130 newEventPipeline().fire(query, channel); 131 try { 132 if (!query.results().isEmpty()) { 133 var json = query.results().get(0).values().stream().findFirst() 134 .get(); 135 HelloWorldModel model 136 = mapper.readValue(json.getBytes(), HelloWorldModel.class); 137 return Optional.of(model); 138 } 139 } catch (InterruptedException | IOException e) { 140 // Means we have no result. 141 } 142 143 // Fall back to creating default state. 144 return createStateRepresentation(event, channel, conletId); 145 } 146 147 @Override 148 protected Set<RenderMode> doRenderConlet(RenderConletRequestBase<?> event, 149 ConsoleConnection channel, String conletId, 150 HelloWorldModel conletState) throws Exception { 151 Set<RenderMode> renderedAs = new HashSet<>(); 152 if (event.renderAs().contains(RenderMode.Preview)) { 153 Template tpl 154 = freemarkerConfig().getTemplate("HelloWorld-preview.ftl.html"); 155 channel.respond(new RenderConlet(type(), conletId, 156 processTemplate(event, tpl, 157 fmModel(event, channel, conletId, conletState))) 158 .setRenderAs( 159 RenderMode.Preview.addModifiers(event.renderAs())) 160 .setSupportedModes(MODES)); 161 renderedAs.add(RenderMode.Preview); 162 } 163 if (event.renderAs().contains(RenderMode.View)) { 164 Template tpl 165 = freemarkerConfig().getTemplate("HelloWorld-view.ftl.html"); 166 channel.respond(new RenderConlet(type(), conletState.getConletId(), 167 processTemplate(event, tpl, 168 fmModel(event, channel, conletId, conletState))) 169 .setRenderAs( 170 RenderMode.View.addModifiers(event.renderAs())) 171 .setSupportedModes(MODES)); 172 channel.respond(new NotifyConletView(type(), 173 conletState.getConletId(), "setWorldVisible", 174 conletState.isWorldVisible())); 175 renderedAs.add(RenderMode.View); 176 } 177 if (event.renderAs().contains(RenderMode.Help)) { 178 Template tpl = freemarkerConfig() 179 .getTemplate("HelloWorld-help.ftl.html"); 180 channel.respond(new OpenModalDialog(type(), conletId, 181 processTemplate(event, tpl, 182 fmModel(event, channel, conletId, conletState))) 183 .addOption("cancelable", true) 184 .addOption("closeLabel", "")); 185 } 186 return renderedAs; 187 } 188 189 @Override 190 protected void doConletDeleted(ConletDeleted event, 191 ConsoleConnection channel, String conletId, 192 HelloWorldModel conletState) throws Exception { 193 if (event.renderModes().isEmpty()) { 194 channel.respond(new KeyValueStoreUpdate().delete( 195 storagePath(channel.session(), conletId))); 196 } 197 } 198 199 @Override 200 protected void doUpdateConletState(NotifyConletModel event, 201 ConsoleConnection channel, HelloWorldModel conletModel) 202 throws Exception { 203 event.stop(); 204 conletModel.setWorldVisible(!conletModel.isWorldVisible()); 205 206 String jsonState = mapper.writer().writeValueAsString(conletModel); 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}