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), params.asArray(1).stream().map(
107                    value -> RenderMode.valueOf((String) value))
108                    .collect(Collectors.toList())),
109                channel);
110            break;
111        }
112        case "deletePortlet": {
113            fire(new DeletePortletRequest(
114                view.renderSupport(), params.asString(0)), channel);
115            break;
116        }
117        case "portalLayout": {
118            List<String> previewLayout = params.asArray(0).stream().map(
119                value -> (String) value).collect(Collectors.toList());
120            List<String> tabsLayout = params.asArray(1).stream().map(
121                value -> (String) value).collect(Collectors.toList());
122            JsonObject xtraInfo = (JsonObject) params.get(2);
123            fire(new PortalLayoutChanged(
124                previewLayout, tabsLayout, xtraInfo), channel);
125            break;
126        }
127        case "renderPortlet": {
128            fire(new RenderPortletRequest(view.renderSupport(),
129                params.asString(0),
130                params.asArray(1).stream().map(
131                    value -> RenderMode.valueOf((String) value))
132                    .collect(Collectors.toList())),
133                channel);
134            break;
135        }
136        case "setLocale": {
137            fire(new SetLocale(Locale.forLanguageTag(params.asString(0))),
138                channel);
139            break;
140        }
141        case "notifyPortletModel": {
142            fire(new NotifyPortletModel(view.renderSupport(),
143                params.asString(0), params.asString(1),
144                params.size() <= 2
145                    ? JsonArray.EMPTY_ARRAY
146                    : params.asArray(2)),
147                channel);
148            break;
149        }
150        default:
151            // Ignore unknown
152            break;
153        }
154    }
155
156    /**
157     * Handle network configured condition.
158     *
159     * @param event the event
160     * @param channel the channel
161     * @throws InterruptedException the interrupted exception
162     * @throws IOException Signals that an I/O exception has occurred.
163     */
164    @Handler
165    public void onPortalConfigured(
166            PortalConfigured event, PortalSession channel)
167            throws InterruptedException, IOException {
168        channel.respond(new SimplePortalCommand("portalConfigured"));
169    }
170
171    /**
172     * Fallback handler that sends a {@link DeletePortlet} event 
173     * if the {@link RenderPortletRequest} event has not been handled
174     * successfully.
175     *
176     * @param event the event
177     * @param channel the channel
178     */
179    @Handler(priority = -1000000)
180    public void onRenderPortlet(
181            RenderPortletRequest event, PortalSession channel) {
182        if (!event.hasBeenRendered()) {
183            channel.respond(new DeletePortlet(event.portletId()));
184        }
185    }
186
187    /**
188     * Discard all portal sessions on stop.
189     *
190     * @param event the event
191     */
192    @Handler
193    public void onStop(Stop event) {
194        for (PortalSession ps : PortalSession.byPortal(this)) {
195            ps.discard();
196        }
197    }
198
199    /**
200     * The MBeans view of a portal.
201     */
202    @SuppressWarnings({ "PMD.CommentRequired", "PMD.AvoidDuplicateLiterals" })
203    public interface PortalMXBean {
204
205        @SuppressWarnings("PMD.CommentRequired")
206        class PortalSessionInfo {
207
208            private final PortalSession session;
209
210            public PortalSessionInfo(PortalSession session) {
211                super();
212                this.session = session;
213            }
214
215            public String getChannel() {
216                return session.upstreamChannel().toString();
217            }
218
219            public String getExpiresAt() {
220                return session.expiresAt().atZone(ZoneId.systemDefault())
221                    .toString();
222            }
223        }
224
225        String getComponentPath();
226
227        String getPrefix();
228
229        boolean isUseMinifiedResources();
230
231        void setUseMinifiedResources(boolean useMinifiedResources);
232
233        SortedMap<String, PortalSessionInfo> getPortalSessions();
234    }
235
236    @SuppressWarnings("PMD.CommentRequired")
237    public static class PortalInfo implements PortalMXBean {
238
239        private static MBeanServer mbs
240            = ManagementFactory.getPlatformMBeanServer();
241
242        private ObjectName mbeanName;
243        private final WeakReference<Portal> portalRef;
244
245        public PortalInfo(Portal portal) {
246            try {
247                mbeanName = new ObjectName("org.jgrapes.portal:type="
248                    + Portal.class.getSimpleName() + ",name="
249                    + ObjectName.quote(Components.simpleObjectName(portal)
250                        + " (" + portal.view.prefix().toString() + ")"));
251            } catch (MalformedObjectNameException e) {
252                // Should not happen
253                logger.log(Level.WARNING, e.getMessage(), e);
254            }
255            portalRef = new WeakReference<>(portal);
256            try {
257                mbs.unregisterMBean(mbeanName);
258            } catch (Exception e) { // NOPMD
259                // Just in case, should not work
260            }
261            try {
262                mbs.registerMBean(this, mbeanName);
263            } catch (InstanceAlreadyExistsException | MBeanRegistrationException
264                    | NotCompliantMBeanException e) {
265                // Should not happen
266                logger.log(Level.WARNING, e.getMessage(), e);
267            }
268        }
269
270        public Optional<Portal> portal() {
271            Portal portal = portalRef.get();
272            if (portal == null) {
273                try {
274                    mbs.unregisterMBean(mbeanName);
275                } catch (Exception e) { // NOPMD
276                    // Should work.
277                }
278            }
279            return Optional.ofNullable(portal);
280        }
281
282        @Override
283        public String getComponentPath() {
284            return portal().map(mgr -> mgr.componentPath()).orElse("<removed>");
285        }
286
287        @Override
288        public String getPrefix() {
289            return portal().map(
290                portal -> portal.view.prefix().toString()).orElse("<unknown>");
291        }
292
293        @Override
294        public boolean isUseMinifiedResources() {
295            return portal().map(
296                portal -> portal.view.useMinifiedResources())
297                .orElse(false);
298        }
299
300        @Override
301        public void setUseMinifiedResources(boolean useMinifiedResources) {
302            portal().ifPresent(portal -> portal.view.setUseMinifiedResources(
303                useMinifiedResources));
304        }
305
306        @Override
307        @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
308        public SortedMap<String, PortalSessionInfo> getPortalSessions() {
309            SortedMap<String, PortalSessionInfo> result = new TreeMap<>();
310            portal().ifPresent(portal -> {
311                for (PortalSession ps : PortalSession.byPortal(portal)) {
312                    result.put(Components.simpleObjectName(ps),
313                        new PortalSessionInfo(ps));
314                }
315            });
316            return result;
317        }
318    }
319
320    /**
321     * An MBean interface for getting information about all portals.
322     * 
323     * There is currently no summary information. However, the (periodic)
324     * invocation of {@link PortalSummaryMXBean#getPortals()} ensures
325     * that entries for removed {@link Portal}s are unregistered.
326     */
327    @SuppressWarnings("PMD.CommentRequired")
328    public interface PortalSummaryMXBean {
329
330        Set<PortalMXBean> getPortals();
331
332    }
333
334    /**
335     * Provides an MBean view of the portal.
336     */
337    @SuppressWarnings("PMD.CommentRequired")
338    private static class MBeanView implements PortalSummaryMXBean {
339
340        private static Set<PortalInfo> portalInfos = new HashSet<>();
341
342        public static void addPortal(Portal portal) {
343            synchronized (portalInfos) {
344                portalInfos.add(new PortalInfo(portal));
345            }
346        }
347
348        @Override
349        public Set<PortalMXBean> getPortals() {
350            Set<PortalInfo> expired = new HashSet<>();
351            synchronized (portalInfos) {
352                for (PortalInfo portalInfo : portalInfos) {
353                    if (!portalInfo.portal().isPresent()) {
354                        expired.add(portalInfo);
355                    }
356                }
357                portalInfos.removeAll(expired);
358            }
359            @SuppressWarnings("unchecked")
360            Set<PortalMXBean> result = (Set<PortalMXBean>) (Object) portalInfos;
361            return result;
362        }
363    }
364
365    static {
366        try {
367            MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
368            ObjectName mxbeanName
369                = new ObjectName("org.jgrapes.portal:type="
370                    + Portal.class.getSimpleName() + "s");
371            mbs.registerMBean(new MBeanView(), mxbeanName);
372        } catch (MalformedObjectNameException | InstanceAlreadyExistsException
373                | MBeanRegistrationException | NotCompliantMBeanException e) {
374            // Should not happen
375            logger.log(Level.WARNING, e.getMessage(), e);
376        }
377    }
378}