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