001/*
002 * JGrapes Event Driven Framework
003 * Copyright (C) 2026 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.helloalpine;
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.nio.charset.StandardCharsets;
029import java.util.EnumSet;
030import java.util.Optional;
031import java.util.Set;
032import org.jgrapes.core.Channel;
033import org.jgrapes.core.Event;
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 * Hello World example using AlpineJS for reactive UI.
057 */
058public class HelloAlpineConlet
059        extends FreeMarkerConlet<HelloAlpineConlet.HelloAlpineModel> {
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     * Instantiates a new hello alpine conlet.
070     *
071     * @param componentChannel the component channel
072     */
073    public HelloAlpineConlet(Channel componentChannel) {
074        super(componentChannel);
075    }
076
077    private String storagePath(Session session, String conletId) {
078        return "/" + WebConsoleUtils.userFromSession(session)
079            .map(ConsoleUser::getName).orElse("")
080            + "/" + HelloAlpineConlet.class.getName() + "/" + conletId;
081    }
082
083    /**
084     * Called when the console is ready and the conlet can be registered.
085     *
086     * @param event the event
087     * @param connection the connection
088     * @throws TemplateNotFoundException the template not found exception
089     * @throws MalformedTemplateNameException the malformed template name exception
090     * @throws ParseException the parse exception
091     * @throws IOException Signals that an I/O exception has occurred.
092     */
093    @Handler
094    public void onConsoleReady(ConsoleReady event, ConsoleConnection connection)
095            throws TemplateNotFoundException, MalformedTemplateNameException,
096            ParseException, IOException {
097        connection.respond(new AddConletType(type())
098            .addRenderMode(RenderMode.Preview).setDisplayNames(
099                localizations(connection.supportedLocales(), "conletName"))
100            .addScript(new ScriptResource()
101                .setScriptType("module").setScriptUri(
102                    event.renderSupport().conletResource(type(),
103                        "HelloAlpine-functions.js")))
104            .addCss(event.renderSupport(), WebConsoleUtils.uriFromPath(
105                "HelloAlpine-style.css")));
106    }
107
108    @Override
109    protected Optional<HelloAlpineModel> createStateRepresentation(
110            Event<?> event, ConsoleConnection channel, String conletId)
111            throws IOException {
112        HelloAlpineModel conletModel = new HelloAlpineModel(conletId);
113        String jsonState = mapper.writer().writeValueAsString(conletModel);
114        channel.respond(new KeyValueStoreUpdate().update(
115            storagePath(channel.session(), conletModel.getConletId()),
116            jsonState));
117        return Optional.of(conletModel);
118    }
119
120    @Override
121    @SuppressWarnings("PMD.EmptyCatchBlock")
122    protected Optional<HelloAlpineModel> recreateState(Event<?> event,
123            ConsoleConnection channel, String conletId) throws Exception {
124        KeyValueStoreQuery query = new KeyValueStoreQuery(
125            storagePath(channel.session(), conletId), channel);
126        newEventPipeline().fire(query, channel);
127        try {
128            if (!query.results().isEmpty()) {
129                var json = query.results().get(0).values().stream().findFirst()
130                    .get();
131                HelloAlpineModel model = mapper.readValue(
132                    json.getBytes(StandardCharsets.UTF_8),
133                    HelloAlpineModel.class);
134                return Optional.of(model);
135            }
136        } catch (InterruptedException | IOException e) {
137            // Means we have no result.
138        }
139        return createStateRepresentation(event, channel, conletId);
140    }
141
142    @Override
143    protected Set<RenderMode> doRenderConlet(RenderConletRequestBase<?> event,
144            ConsoleConnection channel, String conletId,
145            HelloAlpineModel conletState) throws Exception {
146        Set<RenderMode> renderedAs = EnumSet.noneOf(RenderMode.class);
147        if (event.renderAs().contains(RenderMode.Preview)) {
148            Template tpl
149                = freemarkerConfig()
150                    .getTemplate("HelloAlpine-preview.ftl.html");
151            channel.respond(new RenderConlet(type(), conletId,
152                processTemplate(event, tpl,
153                    fmModel(event, channel, conletId, conletState)))
154                        .setRenderAs(
155                            RenderMode.Preview.addModifiers(event.renderAs()))
156                        .setSupportedModes(MODES));
157            renderedAs.add(RenderMode.Preview);
158        }
159        if (event.renderAs().contains(RenderMode.View)) {
160            Template tpl
161                = freemarkerConfig().getTemplate("HelloAlpine-view.ftl.html");
162            channel.respond(new RenderConlet(type(), conletState.getConletId(),
163                processTemplate(event, tpl,
164                    fmModel(event, channel, conletId, conletState)))
165                        .setRenderAs(
166                            RenderMode.View.addModifiers(event.renderAs()))
167                        .setSupportedModes(MODES));
168            channel.respond(new NotifyConletView(type(),
169                conletState.getConletId(), "setWorldVisible",
170                conletState.isWorldVisible()));
171            renderedAs.add(RenderMode.View);
172        }
173        if (event.renderAs().contains(RenderMode.Help)) {
174            Template tpl = freemarkerConfig()
175                .getTemplate("HelloAlpine-help.ftl.html");
176            channel.respond(new OpenModalDialog(type(), conletId,
177                processTemplate(event, tpl,
178                    fmModel(event, channel, conletId, conletState)))
179                        .addOption("cancelable", true)
180                        .addOption("closeLabel", ""));
181        }
182        return renderedAs;
183    }
184
185    @Override
186    protected void doConletDeleted(ConletDeleted event,
187            ConsoleConnection channel, String conletId,
188            HelloAlpineModel conletState) throws Exception {
189        if (event.renderModes().isEmpty()) {
190            channel.respond(new KeyValueStoreUpdate().delete(
191                storagePath(channel.session(), conletId)));
192        }
193    }
194
195    @Override
196    protected void doUpdateConletState(NotifyConletModel event,
197            ConsoleConnection channel, HelloAlpineModel conletModel)
198            throws Exception {
199        event.stop();
200        conletModel.setWorldVisible(!conletModel.isWorldVisible());
201
202        String jsonState = mapper.writer().writeValueAsString(conletModel);
203        channel.respond(new KeyValueStoreUpdate().update(
204            storagePath(channel.session(), conletModel.getConletId()),
205            jsonState));
206        channel.respond(new NotifyConletView(type(),
207            conletModel.getConletId(), "setWorldVisible",
208            conletModel.isWorldVisible()));
209        channel.respond(new DisplayNotification("<span>"
210            + resourceBundle(channel.locale()).getString("visibilityChange")
211            + "</span>")
212                .addOption("autoClose", 2000));
213    }
214
215    /**
216     * Model with world's state.
217     */
218    public static class HelloAlpineModel extends ConletBaseModel {
219
220        private boolean worldVisible = true;
221
222        /**
223         * Instantiates a new hello alpine model.
224         *
225         * @param conletId the conlet id
226         */
227        @ConstructorProperties({ "conletId" })
228        public HelloAlpineModel(String conletId) {
229            super(conletId);
230        }
231
232        public void setWorldVisible(boolean visible) {
233            this.worldVisible = visible;
234        }
235
236        public boolean isWorldVisible() {
237            return worldVisible;
238        }
239    }
240}