001/*
002 * JGrapes Event Driven Framework
003 * Copyright (C) 2025 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.hellosolid;
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 HelloSolidConlet
059        extends FreeMarkerConlet<HelloSolidConlet.HelloSolidModel> {
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 HelloSolidConlet(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            + "/" + HelloSolidConlet.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                .setScriptUri(event.renderSupport().conletResource(
106                    type(), "HelloSolid-functions.js"))
107                .setScriptType("module")));
108//            .addScript(new ScriptResource()
109//                .setScriptType("module").setScriptUri(
110//                    event.renderSupport().conletResource(type(),
111//                        "HelloSolid-functions.js")))
112//            .addCss(event.renderSupport(), WebConsoleUtils.uriFromPath(
113//                "HelloSolid-style.css")));
114    }
115
116    @Override
117    protected Optional<HelloSolidModel> createStateRepresentation(
118            Event<?> event, ConsoleConnection channel, String conletId)
119            throws IOException {
120        HelloSolidModel conletModel = new HelloSolidModel(conletId);
121        String jsonState = mapper.writer().writeValueAsString(conletModel);
122        channel.respond(new KeyValueStoreUpdate().update(
123            storagePath(channel.session(), conletModel.getConletId()),
124            jsonState));
125        return Optional.of(conletModel);
126    }
127
128    @Override
129    @SuppressWarnings("PMD.EmptyCatchBlock")
130    protected Optional<HelloSolidModel> recreateState(Event<?> event,
131            ConsoleConnection channel, String conletId) throws Exception {
132        KeyValueStoreQuery query = new KeyValueStoreQuery(
133            storagePath(channel.session(), conletId), channel);
134        newEventPipeline().fire(query, channel);
135        try {
136            if (!query.results().isEmpty()) {
137                var json = query.results().get(0).values().stream().findFirst()
138                    .get();
139                HelloSolidModel model
140                    = mapper.readValue(json.getBytes(), HelloSolidModel.class);
141                return Optional.of(model);
142            }
143        } catch (InterruptedException | IOException e) {
144            // Means we have no result.
145        }
146
147        // Fall back to creating default state.
148        return createStateRepresentation(event, channel, conletId);
149    }
150
151    @Override
152    protected Set<RenderMode> doRenderConlet(RenderConletRequestBase<?> event,
153            ConsoleConnection channel, String conletId,
154            HelloSolidModel conletState) throws Exception {
155        Set<RenderMode> renderedAs = new HashSet<>();
156        if (event.renderAs().contains(RenderMode.Preview)) {
157            Template tpl
158                = freemarkerConfig().getTemplate("HelloSolid-preview.ftl.html");
159            channel.respond(new RenderConlet(type(), conletId,
160                processTemplate(event, tpl,
161                    fmModel(event, channel, conletId, conletState)))
162                        .setRenderAs(
163                            RenderMode.Preview.addModifiers(event.renderAs()))
164                        .setSupportedModes(MODES));
165            renderedAs.add(RenderMode.Preview);
166        }
167        if (event.renderAs().contains(RenderMode.View)) {
168            Template tpl
169                = freemarkerConfig().getTemplate("HelloSolid-view.ftl.html");
170            channel.respond(new RenderConlet(type(), conletState.getConletId(),
171                processTemplate(event, tpl,
172                    fmModel(event, channel, conletId, conletState)))
173                        .setRenderAs(
174                            RenderMode.View.addModifiers(event.renderAs()))
175                        .setSupportedModes(MODES));
176            channel.respond(new NotifyConletView(type(),
177                conletState.getConletId(), "setWorldVisible",
178                conletState.isWorldVisible()));
179            renderedAs.add(RenderMode.View);
180        }
181        if (event.renderAs().contains(RenderMode.Help)) {
182            Template tpl = freemarkerConfig()
183                .getTemplate("HelloSolid-help.ftl.html");
184            channel.respond(new OpenModalDialog(type(), conletId,
185                processTemplate(event, tpl,
186                    fmModel(event, channel, conletId, conletState)))
187                        .addOption("cancelable", true)
188                        .addOption("closeLabel", ""));
189        }
190        return renderedAs;
191    }
192
193    @Override
194    protected void doConletDeleted(ConletDeleted event,
195            ConsoleConnection channel, String conletId,
196            HelloSolidModel conletState) throws Exception {
197        if (event.renderModes().isEmpty()) {
198            channel.respond(new KeyValueStoreUpdate().delete(
199                storagePath(channel.session(), conletId)));
200        }
201    }
202
203    @Override
204    protected void doUpdateConletState(NotifyConletModel event,
205            ConsoleConnection channel, HelloSolidModel conletModel)
206            throws Exception {
207        event.stop();
208        conletModel.setWorldVisible(!conletModel.isWorldVisible());
209
210        String jsonState = mapper.writer().writeValueAsString(conletModel);
211        channel.respond(new KeyValueStoreUpdate().update(
212            storagePath(channel.session(), conletModel.getConletId()),
213            jsonState));
214        channel.respond(new NotifyConletView(type(),
215            conletModel.getConletId(), "setWorldVisible",
216            conletModel.isWorldVisible()));
217        channel.respond(new DisplayNotification("<span>"
218            + resourceBundle(channel.locale()).getString("visibilityChange")
219            + "</span>")
220                .addOption("autoClose", 2000));
221    }
222
223    /**
224     * Model with world's state.
225     */
226    public static class HelloSolidModel extends ConletBaseModel {
227
228        private boolean worldVisible = true;
229
230        /**
231         * Creates a new model with the given type and id.
232         * 
233         * @param conletId the web console component id
234         */
235        @ConstructorProperties({ "conletId" })
236        public HelloSolidModel(String conletId) {
237            super(conletId);
238        }
239
240        /**
241         * @param visible the visible to set
242         */
243        public void setWorldVisible(boolean visible) {
244            this.worldVisible = visible;
245        }
246
247        public boolean isWorldVisible() {
248            return worldVisible;
249        }
250    }
251
252}