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}