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