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