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.portal.base;
020
021import java.io.IOException;
022import java.util.Arrays;
023import java.util.Collections;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027import java.util.Optional;
028
029import org.jdrupes.json.JsonBeanDecoder;
030import org.jdrupes.json.JsonBeanEncoder;
031import org.jdrupes.json.JsonDecodeException;
032import org.jdrupes.json.JsonObject;
033import org.jgrapes.core.Channel;
034import org.jgrapes.core.Component;
035import org.jgrapes.core.annotation.Handler;
036import org.jgrapes.http.Session;
037import org.jgrapes.io.IOSubchannel;
038import org.jgrapes.portal.base.Portlet.RenderMode;
039import org.jgrapes.portal.base.events.LastPortalLayout;
040import org.jgrapes.portal.base.events.PortalLayoutChanged;
041import org.jgrapes.portal.base.events.PortalPrepared;
042import org.jgrapes.portal.base.events.PortalReady;
043import org.jgrapes.portal.base.events.RenderPortletRequest;
044import org.jgrapes.util.events.KeyValueStoreData;
045import org.jgrapes.util.events.KeyValueStoreQuery;
046import org.jgrapes.util.events.KeyValueStoreUpdate;
047
048/**
049 * A component that restores the portal layout,
050 * using key/value events for persisting the data between sessions.
051 * 
052 * ![Boot Event Sequence](KVPPBootSeq.svg)
053 * 
054 * This component requires another component that handles the key/value
055 * store events ({@link KeyValueStoreUpdate}, {@link KeyValueStoreQuery})
056 * used by this component for implementing persistence. When the portal becomes
057 * ready, this policy sends a query for the persisted data.
058 * 
059 * When the portal has been prepared, the policy sends the last layout
060 * as retrieved from persistent storage to the portal and then generates
061 * render events for all portlets contained in this layout.
062 * 
063 * Each time the layout is changed in the portal, the portal sends the
064 * new layout data and this component updates the persistent storage
065 * accordingly.
066 * 
067 * @startuml KVPPBootSeq.svg
068 * hide footbox
069 * 
070 * Browser -> Portal: "portalReady"
071 * activate Portal
072 * Portal -> KVStoreBasedPortalPolicy: PortalReady
073 * deactivate Portal
074 * activate KVStoreBasedPortalPolicy
075 * KVStoreBasedPortalPolicy -> "KV Store": KeyValueStoreQuery
076 * activate "KV Store"
077 * "KV Store" -> KVStoreBasedPortalPolicy: KeyValueStoreData
078 * deactivate "KV Store"
079 * deactivate KVStoreBasedPortalPolicy
080 * 
081 * actor System
082 * System -> KVStoreBasedPortalPolicy: PortalPrepared
083 * activate KVStoreBasedPortalPolicy
084 * KVStoreBasedPortalPolicy -> Portal: LastPortalLayout
085 * activate Portal
086 * Portal -> Browser: "lastPortalLayout"
087 * deactivate Portal
088 * loop for all portlets to be displayed
089 *     KVStoreBasedPortalPolicy -> PortletX: RenderPortletRequest
090 *     activate PortletX
091 *     PortletX -> Portal: RenderPortlet
092 *     deactivate PortletX
093 *     activate Portal
094 *     Portal -> Browser: "renderPortlet"
095 *     deactivate Portal
096 * end
097 * deactivate KVStoreBasedPortalPolicy
098 * 
099 * Browser -> Portal: "portalLayout"
100 * activate Portal
101 * Portal -> KVStoreBasedPortalPolicy: PortalLayoutChanged
102 * deactivate Portal
103 * activate KVStoreBasedPortalPolicy
104 * KVStoreBasedPortalPolicy -> "KV Store": KeyValueStoreUpdate
105 * deactivate KVStoreBasedPortalPolicy
106 * 
107 * @enduml
108 */
109public class KVStoreBasedPortalPolicy extends Component {
110
111    /**
112     * Creates a new component with its channel set to
113     * itself.
114     */
115    public KVStoreBasedPortalPolicy() {
116        // Everything done by super.
117    }
118
119    /**
120     * Creates a new component with its channel set to the given channel.
121     * 
122     * @param componentChannel
123     */
124    public KVStoreBasedPortalPolicy(Channel componentChannel) {
125        super(componentChannel);
126    }
127
128    /**
129     * Intercept the {@link PortalReady} event. Request the 
130     * session data from the key/value store and resume.
131     * 
132     * @param event
133     * @param channel
134     * @throws InterruptedException
135     */
136    @Handler
137    public void onPortalReady(PortalReady event, PortalSession channel)
138            throws InterruptedException {
139        PortalSessionDataStore sessionDs = channel.associated(
140            PortalSessionDataStore.class,
141            () -> new PortalSessionDataStore(channel.browserSession()));
142        sessionDs.onPortalReady(event, channel);
143    }
144
145    /**
146     * Handle returned data.
147     *
148     * @param event the event
149     * @param channel the channel
150     * @throws JsonDecodeException the json decode exception
151     */
152    @Handler
153    public void onKeyValueStoreData(
154            KeyValueStoreData event, PortalSession channel)
155            throws JsonDecodeException {
156        Optional<PortalSessionDataStore> optSessionDs
157            = channel.associated(PortalSessionDataStore.class);
158        if (optSessionDs.isPresent()) {
159            optSessionDs.get().onKeyValueStoreData(event, channel);
160        }
161    }
162
163    /**
164     * Handle portal page loaded.
165     *
166     * @param event the event
167     * @param channel the channel
168     */
169    @Handler
170    public void onPortalPrepared(
171            PortalPrepared event, PortalSession channel) {
172        channel.associated(PortalSessionDataStore.class).ifPresent(
173            psess -> psess.onPortalPrepared(event, channel));
174    }
175
176    /**
177     * Handle changed layout.
178     *
179     * @param event the event
180     * @param channel the channel
181     * @throws IOException Signals that an I/O exception has occurred.
182     */
183    @Handler
184    public void onPortalLayoutChanged(PortalLayoutChanged event,
185            PortalSession channel) throws IOException {
186        Optional<PortalSessionDataStore> optDs = channel.associated(
187            PortalSessionDataStore.class);
188        if (optDs.isPresent()) {
189            optDs.get().onPortalLayoutChanged(event, channel);
190        }
191    }
192
193    /**
194     * Stores the data for the portal session.
195     */
196    @SuppressWarnings("PMD.CommentRequired")
197    private class PortalSessionDataStore {
198
199        private final String storagePath;
200        private Map<String, Object> persisted;
201
202        public PortalSessionDataStore(Session session) {
203            storagePath = "/"
204                + PortalUtils.userFromSession(session)
205                    .map(UserPrincipal::toString).orElse("")
206                + "/" + KVStoreBasedPortalPolicy.class.getName();
207        }
208
209        public void onPortalReady(PortalReady event, IOSubchannel channel)
210                throws InterruptedException {
211            if (persisted != null) {
212                return;
213            }
214            KeyValueStoreQuery query = new KeyValueStoreQuery(
215                storagePath, channel);
216            fire(query, channel);
217        }
218
219        public void onKeyValueStoreData(
220                KeyValueStoreData event, IOSubchannel channel)
221                throws JsonDecodeException {
222            if (!event.event().query().equals(storagePath)) {
223                return;
224            }
225            String data = event.data().get(storagePath);
226            if (data != null) {
227                JsonBeanDecoder decoder = JsonBeanDecoder.create(data);
228                @SuppressWarnings({ "unchecked", "PMD.LooseCoupling" })
229                Class<Map<String, Object>> cls
230                    = (Class<Map<String, Object>>) (Class<?>) HashMap.class;
231                persisted = decoder.readObject(cls);
232            }
233        }
234
235        @SuppressWarnings({ "PMD.DataflowAnomalyAnalysis",
236            "PMD.AvoidInstantiatingObjectsInLoops" })
237        public void onPortalPrepared(
238                PortalPrepared event, IOSubchannel channel) {
239            if (persisted == null) {
240                // Retrieval was not successful
241                persisted = new HashMap<>();
242            }
243            // Make sure data is consistent
244            @SuppressWarnings("unchecked")
245            List<String> previewLayout = (List<String>) persisted
246                .computeIfAbsent("previewLayout",
247                    newKey -> {
248                        return Collections.emptyList();
249                    });
250            @SuppressWarnings("unchecked")
251            List<String> tabsLayout = (List<String>) persisted.computeIfAbsent(
252                "tabsLayout", newKey -> {
253                    return Collections.emptyList();
254                });
255            JsonObject xtraInfo = (JsonObject) persisted.computeIfAbsent(
256                "xtraInfo", newKey -> {
257                    return JsonObject.create();
258                });
259
260            // Update layout
261            channel.respond(new LastPortalLayout(
262                previewLayout, tabsLayout, xtraInfo));
263
264            // Restore portlets
265            for (String portletId : tabsLayout) {
266                fire(new RenderPortletRequest(
267                    event.event().renderSupport(), portletId,
268                    Arrays.asList(new RenderMode[] { RenderMode.View })),
269                    channel);
270            }
271            for (String portletId : previewLayout) {
272                fire(new RenderPortletRequest(
273                    event.event().renderSupport(), portletId,
274                    Arrays.asList(new RenderMode[] { RenderMode.Foreground })),
275                    channel);
276            }
277        }
278
279        public void onPortalLayoutChanged(PortalLayoutChanged event,
280                IOSubchannel channel) throws IOException {
281            persisted.put("previewLayout", event.previewLayout());
282            persisted.put("tabsLayout", event.tabsLayout());
283            persisted.put("xtraInfo", event.xtraInfo());
284
285            // Now store.
286            JsonBeanEncoder encoder = JsonBeanEncoder.create();
287            encoder.writeObject(persisted);
288            fire(new KeyValueStoreUpdate()
289                .update(storagePath, encoder.toJson()), channel);
290        }
291
292    }
293}