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