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}