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.lang.management.ManagementFactory;
023import java.lang.ref.WeakReference;
024import java.time.ZoneId;
025import java.util.HashSet;
026import java.util.List;
027import java.util.Locale;
028import java.util.Optional;
029import java.util.Set;
030import java.util.SortedMap;
031import java.util.TreeMap;
032import java.util.logging.Level;
033import java.util.logging.Logger;
034import java.util.stream.Collectors;
035
036import javax.management.InstanceAlreadyExistsException;
037import javax.management.MBeanRegistrationException;
038import javax.management.MBeanServer;
039import javax.management.MalformedObjectNameException;
040import javax.management.NotCompliantMBeanException;
041import javax.management.ObjectName;
042
043import org.jdrupes.json.JsonArray;
044import org.jdrupes.json.JsonObject;
045import org.jgrapes.core.Channel;
046import org.jgrapes.core.Component;
047import org.jgrapes.core.Components;
048import org.jgrapes.core.annotation.Handler;
049import org.jgrapes.core.events.Stop;
050import org.jgrapes.portal.base.Portlet.RenderMode;
051import org.jgrapes.portal.base.events.AddPortletRequest;
052import org.jgrapes.portal.base.events.DeletePortlet;
053import org.jgrapes.portal.base.events.DeletePortletRequest;
054import org.jgrapes.portal.base.events.JsonInput;
055import org.jgrapes.portal.base.events.NotifyPortletModel;
056import org.jgrapes.portal.base.events.PortalConfigured;
057import org.jgrapes.portal.base.events.PortalLayoutChanged;
058import org.jgrapes.portal.base.events.PortalReady;
059import org.jgrapes.portal.base.events.RenderPortletRequest;
060import org.jgrapes.portal.base.events.SetLocale;
061import org.jgrapes.portal.base.events.SimplePortalCommand;
062
063/**
064 * Provides the portlet related part of the portal.
065 */
066public class Portal extends Component {
067
068    private static final Logger logger
069        = Logger.getLogger(Portal.class.getName());
070
071    private PortalWeblet view;
072
073    /**
074     * @param componentChannel
075     */
076    /* default */ Portal(Channel componentChannel) {
077        super(componentChannel);
078    }
079
080    /* default */ void setView(PortalWeblet view) {
081        this.view = view;
082        MBeanView.addPortal(this);
083    }
084
085    /**
086     * Handle JSON input.
087     *
088     * @param event the event
089     * @param channel the channel
090     * @throws InterruptedException the interrupted exception
091     * @throws IOException Signals that an I/O exception has occurred.
092     */
093    @Handler
094    @SuppressWarnings("PMD.DataflowAnomalyAnalysis")
095    public void onJsonInput(JsonInput event, PortalSession channel)
096            throws InterruptedException, IOException {
097        // Send events to portlets on portal's channel
098        JsonArray params = event.request().params();
099        switch (event.request().method()) {
100        case "portalReady": {
101            fire(new PortalReady(view.renderSupport()), channel);
102            break;
103        }
104        case "addPortlet": {
105            fire(new AddPortletRequest(view.renderSupport(),
106                params.asString(0), RenderMode.valueOf(
107                    params.asString(1))),
108                channel);
109            break;
110        }
111        case "deletePortlet": {
112            fire(new DeletePortletRequest(
113                view.renderSupport(), params.asString(0)), channel);
114            break;
115        }
116        case "portalLayout": {
117            List<String> previewLayout = params.asArray(0).stream().map(
118                value -> (String) value).collect(Collectors.toList());
119            List<String> tabsLayout = params.asArray(1).stream().map(
120                value -> (String) value).collect(Collectors.toList());
121            JsonObject xtraInfo = (JsonObject) params.get(2);
122            fire(new PortalLayoutChanged(
123                previewLayout, tabsLayout, xtraInfo), channel);
124            break;
125        }
126        case "renderPortlet": {
127            fire(new RenderPortletRequest(view.renderSupport(),
128                params.asString(0),
129                RenderMode.valueOf(params.asString(1)),
130                (Boolean) params.asBoolean(2)), channel);
131            break;
132        }
133        case "setLocale": {
134            fire(new SetLocale(Locale.forLanguageTag(params.asString(0))),
135                channel);
136            break;
137        }
138        case "notifyPortletModel": {
139            fire(new NotifyPortletModel(view.renderSupport(),
140                params.asString(0), params.asString(1),
141                params.size() <= 2
142                    ? JsonArray.EMPTY_ARRAY
143                    : params.asArray(2)),
144                channel);
145            break;
146        }
147        default:
148            // Ignore unknown
149            break;
150        }
151    }
152
153    /**
154     * Handle network configured condition.
155     *
156     * @param event the event
157     * @param channel the channel
158     * @throws InterruptedException the interrupted exception
159     * @throws IOException Signals that an I/O exception has occurred.
160     */
161    @Handler
162    public void onPortalConfigured(
163            PortalConfigured event, PortalSession channel)
164            throws InterruptedException, IOException {
165        channel.respond(new SimplePortalCommand("portalConfigured"));
166    }
167
168    /**
169     * Fallback handler that sends a {@link DeletePortlet} event 
170     * if the {@link RenderPortletRequest} event has not been handled
171     * successfully.
172     *
173     * @param event the event
174     * @param channel the channel
175     */
176    @Handler(priority = -1000000)
177    public void onRenderPortlet(
178            RenderPortletRequest event, PortalSession channel) {
179        if (!event.hasBeenRendered()) {
180            channel.respond(new DeletePortlet(event.portletId()));
181        }
182    }
183
184    /**
185     * Discard all portal sessions on stop.
186     *
187     * @param event the event
188     */
189    @Handler
190    public void onStop(Stop event) {
191        for (PortalSession ps : PortalSession.byPortal(this)) {
192            ps.discard();
193        }
194    }
195
196    /**
197     * The MBeans view of a portal.
198     */
199    @SuppressWarnings({ "PMD.CommentRequired", "PMD.AvoidDuplicateLiterals" })
200    public interface PortalMXBean {
201
202        @SuppressWarnings("PMD.CommentRequired")
203        class PortalSessionInfo {
204
205            private final PortalSession session;
206
207            public PortalSessionInfo(PortalSession session) {
208                super();
209                this.session = session;
210            }
211
212            public String getChannel() {
213                return session.upstreamChannel().toString();
214            }
215
216            public String getExpiresAt() {
217                return session.expiresAt().atZone(ZoneId.systemDefault())
218                    .toString();
219            }
220        }
221
222        String getComponentPath();
223
224        String getPrefix();
225
226        boolean isUseMinifiedResources();
227
228        void setUseMinifiedResources(boolean useMinifiedResources);
229
230        SortedMap<String, PortalSessionInfo> getPortalSessions();
231    }
232
233    @SuppressWarnings("PMD.CommentRequired")
234    public static class PortalInfo implements PortalMXBean {
235
236        private static MBeanServer mbs
237            = ManagementFactory.getPlatformMBeanServer();
238
239        private ObjectName mbeanName;
240        private final WeakReference<Portal> portalRef;
241
242        public PortalInfo(Portal portal) {
243            try {
244                mbeanName = new ObjectName("org.jgrapes.portal:type="
245                    + Portal.class.getSimpleName() + ",name="
246                    + ObjectName.quote(Components.simpleObjectName(portal)
247                        + " (" + portal.view.prefix().toString() + ")"));
248            } catch (MalformedObjectNameException e) {
249                // Should not happen
250                logger.log(Level.WARNING, e.getMessage(), e);
251            }
252            portalRef = new WeakReference<>(portal);
253            try {
254                mbs.unregisterMBean(mbeanName);
255            } catch (Exception e) { // NOPMD
256                // Just in case, should not work
257            }
258            try {
259                mbs.registerMBean(this, mbeanName);
260            } catch (InstanceAlreadyExistsException | MBeanRegistrationException
261                    | NotCompliantMBeanException e) {
262                // Should not happen
263                logger.log(Level.WARNING, e.getMessage(), e);
264            }
265        }
266
267        public Optional<Portal> portal() {
268            Portal portal = portalRef.get();
269            if (portal == null) {
270                try {
271                    mbs.unregisterMBean(mbeanName);
272                } catch (Exception e) { // NOPMD
273                    // Should work.
274                }
275            }
276            return Optional.ofNullable(portal);
277        }
278
279        @Override
280        public String getComponentPath() {
281            return portal().map(mgr -> mgr.componentPath()).orElse("<removed>");
282        }
283
284        @Override
285        public String getPrefix() {
286            return portal().map(
287                portal -> portal.view.prefix().toString()).orElse("<unknown>");
288        }
289
290        @Override
291        public boolean isUseMinifiedResources() {
292            return portal().map(
293                portal -> portal.view.useMinifiedResources())
294                .orElse(false);
295        }
296
297        @Override
298        public void setUseMinifiedResources(boolean useMinifiedResources) {
299            portal().ifPresent(portal -> portal.view.setUseMinifiedResources(
300                useMinifiedResources));
301        }
302
303        @Override
304        @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
305        public SortedMap<String, PortalSessionInfo> getPortalSessions() {
306            SortedMap<String, PortalSessionInfo> result = new TreeMap<>();
307            portal().ifPresent(portal -> {
308                for (PortalSession ps : PortalSession.byPortal(portal)) {
309                    result.put(Components.simpleObjectName(ps),
310                        new PortalSessionInfo(ps));
311                }
312            });
313            return result;
314        }
315    }
316
317    /**
318     * An MBean interface for getting information about all portals.
319     * 
320     * There is currently no summary information. However, the (periodic)
321     * invocation of {@link PortalSummaryMXBean#getPortals()} ensures
322     * that entries for removed {@link Portal}s are unregistered.
323     */
324    @SuppressWarnings("PMD.CommentRequired")
325    public interface PortalSummaryMXBean {
326
327        Set<PortalMXBean> getPortals();
328
329    }
330
331    /**
332     * Provides an MBean view of the portal.
333     */
334    @SuppressWarnings("PMD.CommentRequired")
335    private static class MBeanView implements PortalSummaryMXBean {
336
337        private static Set<PortalInfo> portalInfos = new HashSet<>();
338
339        public static void addPortal(Portal portal) {
340            synchronized (portalInfos) {
341                portalInfos.add(new PortalInfo(portal));
342            }
343        }
344
345        @Override
346        public Set<PortalMXBean> getPortals() {
347            Set<PortalInfo> expired = new HashSet<>();
348            synchronized (portalInfos) {
349                for (PortalInfo portalInfo : portalInfos) {
350                    if (!portalInfo.portal().isPresent()) {
351                        expired.add(portalInfo);
352                    }
353                }
354                portalInfos.removeAll(expired);
355            }
356            @SuppressWarnings("unchecked")
357            Set<PortalMXBean> result = (Set<PortalMXBean>) (Object) portalInfos;
358            return result;
359        }
360    }
361
362    static {
363        try {
364            MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
365            ObjectName mxbeanName
366                = new ObjectName("org.jgrapes.portal:type="
367                    + Portal.class.getSimpleName() + "s");
368            mbs.registerMBean(new MBeanView(), mxbeanName);
369        } catch (MalformedObjectNameException | InstanceAlreadyExistsException
370                | MBeanRegistrationException | NotCompliantMBeanException e) {
371            // Should not happen
372            logger.log(Level.WARNING, e.getMessage(), e);
373        }
374    }
375}