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.KeyValueStoreData;
039import org.jgrapes.util.events.KeyValueStoreQuery;
040import org.jgrapes.util.events.KeyValueStoreUpdate;
041import org.jgrapes.webconsole.base.Conlet.RenderMode;
042import org.jgrapes.webconsole.base.ConletBaseModel;
043import org.jgrapes.webconsole.base.ConsoleSession;
044import org.jgrapes.webconsole.base.UserPrincipal;
045import org.jgrapes.webconsole.base.WebConsoleUtils;
046import org.jgrapes.webconsole.base.events.AddConletType;
047import org.jgrapes.webconsole.base.events.AddPageResources.ScriptResource;
048import org.jgrapes.webconsole.base.events.ConletDeleted;
049import org.jgrapes.webconsole.base.events.ConsoleReady;
050import org.jgrapes.webconsole.base.events.DisplayNotification;
051import org.jgrapes.webconsole.base.events.NotifyConletModel;
052import org.jgrapes.webconsole.base.events.NotifyConletView;
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);
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) {
079        return "/" + WebConsoleUtils.userFromSession(session)
080            .map(UserPrincipal::toString).orElse("")
081            + "/conlets/" + HelloWorldConlet.class.getName() + "/";
082    }
083
084    /**
085     * Trigger loading of resources when the console is ready.
086     *
087     * @param event the event
088     * @param consoleSession the console session
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,
096            ConsoleSession consoleSession)
097            throws TemplateNotFoundException, MalformedTemplateNameException,
098            ParseException, IOException {
099        // Add HelloWorldConlet resources to page
100        consoleSession.respond(new AddConletType(type())
101            .addRenderMode(RenderMode.Preview).setDisplayNames(
102                localizations(consoleSession.supportedLocales(), "conletName"))
103            .addScript(new ScriptResource().setScriptUri(
104                event.renderSupport().conletResource(type(),
105                    "HelloWorld-functions.js")))
106            .addCss(event.renderSupport(), WebConsoleUtils.uriFromPath(
107                "HelloWorld-style.css")));
108        KeyValueStoreQuery query = new KeyValueStoreQuery(
109            storagePath(consoleSession.browserSession()), consoleSession);
110        fire(query, consoleSession);
111    }
112
113    /**
114     * Invoked when the key/value store provides data.
115     *
116     * @param event the event
117     * @param channel the channel
118     * @throws JsonDecodeException the json decode exception
119     */
120    @Handler
121    public void onKeyValueStoreData(
122            KeyValueStoreData event, ConsoleSession channel)
123            throws JsonDecodeException {
124        if (!event.event().query()
125            .equals(storagePath(channel.browserSession()))) {
126            return;
127        }
128        for (String json : event.data().values()) {
129            HelloWorldModel model = JsonBeanDecoder.create(json)
130                .readObject(HelloWorldModel.class);
131            putInSession(channel.browserSession(), model.getConletId(), model);
132        }
133    }
134
135    @Override
136    protected Optional<HelloWorldModel> createStateRepresentation(
137            RenderConletRequestBase<?> event,
138            ConsoleSession channel, String conletId) throws IOException {
139        HelloWorldModel conletModel = new HelloWorldModel(conletId);
140        String jsonState
141            = JsonBeanEncoder.create().writeObject(conletModel).toJson();
142        channel.respond(new KeyValueStoreUpdate().update(
143            storagePath(channel.browserSession()) + conletModel.getConletId(),
144            jsonState));
145        return Optional.of(conletModel);
146    }
147
148    @Override
149    protected Set<RenderMode> doRenderConlet(RenderConletRequestBase<?> event,
150            ConsoleSession channel, String conletId,
151            HelloWorldModel conletState) throws Exception {
152        Set<RenderMode> renderedAs = new HashSet<>();
153        if (event.renderAs().contains(RenderMode.Preview)) {
154            Template tpl
155                = freemarkerConfig().getTemplate("HelloWorld-preview.ftl.html");
156            channel.respond(new RenderConlet(type(), conletId,
157                processTemplate(event, tpl,
158                    fmModel(event, channel, conletId, conletState)))
159                        .setRenderAs(
160                            RenderMode.Preview.addModifiers(event.renderAs()))
161                        .setSupportedModes(MODES));
162            renderedAs.add(RenderMode.Preview);
163        }
164        if (event.renderAs().contains(RenderMode.View)) {
165            Template tpl
166                = freemarkerConfig().getTemplate("HelloWorld-view.ftl.html");
167            channel.respond(new RenderConlet(type(), conletState.getConletId(),
168                processTemplate(event, tpl,
169                    fmModel(event, channel, conletId, conletState)))
170                        .setRenderAs(
171                            RenderMode.View.addModifiers(event.renderAs()))
172                        .setSupportedModes(MODES));
173            channel.respond(new NotifyConletView(type(),
174                conletState.getConletId(), "setWorldVisible",
175                conletState.isWorldVisible()));
176            renderedAs.add(RenderMode.View);
177        }
178        return renderedAs;
179    }
180
181    @Override
182    protected void doConletDeleted(ConletDeleted event,
183            ConsoleSession channel, String conletId,
184            HelloWorldModel conletState) throws Exception {
185        if (event.renderModes().isEmpty()) {
186            channel.respond(new KeyValueStoreUpdate().delete(
187                storagePath(channel.browserSession()) + conletId));
188        }
189    }
190
191    @Override
192    protected void doUpdateConletState(NotifyConletModel event,
193            ConsoleSession channel, HelloWorldModel conletModel)
194            throws Exception {
195        event.stop();
196        conletModel.setWorldVisible(!conletModel.isWorldVisible());
197
198        String jsonState = JsonBeanEncoder.create()
199            .writeObject(conletModel).toJson();
200        channel.respond(new KeyValueStoreUpdate().update(
201            storagePath(channel.browserSession()) + conletModel.getConletId(),
202            jsonState));
203        channel.respond(new NotifyConletView(type(),
204            conletModel.getConletId(), "setWorldVisible",
205            conletModel.isWorldVisible()));
206        channel.respond(new DisplayNotification("<span>"
207            + resourceBundle(channel.locale()).getString("visibilityChange")
208            + "</span>")
209                .addOption("autoClose", 2000));
210    }
211
212    /**
213     * Model with world's state.
214     */
215    @SuppressWarnings("serial")
216    public static class HelloWorldModel extends ConletBaseModel {
217
218        private boolean worldVisible = true;
219
220        /**
221         * Creates a new model with the given type and id.
222         * 
223         * @param conletId the web console component id
224         */
225        @ConstructorProperties({ "conletId" })
226        public HelloWorldModel(String conletId) {
227            super(conletId);
228        }
229
230        /**
231         * @param visible the visible to set
232         */
233        public void setWorldVisible(boolean visible) {
234            this.worldVisible = visible;
235        }
236
237        public boolean isWorldVisible() {
238            return worldVisible;
239        }
240    }
241
242}