001/*
002 * JGrapes Event Driven Framework
003 * Copyright (C) 2016-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.net;
020
021import java.io.IOException;
022import java.lang.management.ManagementFactory;
023import java.lang.ref.WeakReference;
024import java.net.InetSocketAddress;
025import java.net.SocketAddress;
026import java.net.StandardProtocolFamily;
027import java.net.UnixDomainSocketAddress;
028import java.nio.channels.SelectionKey;
029import java.nio.channels.ServerSocketChannel;
030import java.nio.channels.SocketChannel;
031import java.util.ArrayList;
032import java.util.Collections;
033import java.util.Comparator;
034import java.util.HashSet;
035import java.util.IntSummaryStatistics;
036import java.util.List;
037import java.util.Optional;
038import java.util.Set;
039import java.util.SortedMap;
040import java.util.TreeMap;
041import java.util.stream.Collectors;
042import javax.management.InstanceAlreadyExistsException;
043import javax.management.MBeanRegistrationException;
044import javax.management.MBeanServer;
045import javax.management.MalformedObjectNameException;
046import javax.management.NotCompliantMBeanException;
047import javax.management.ObjectName;
048import org.jgrapes.core.Channel;
049import org.jgrapes.core.Components;
050import org.jgrapes.core.Event;
051import org.jgrapes.core.Manager;
052import org.jgrapes.core.Self;
053import org.jgrapes.core.Subchannel;
054import org.jgrapes.core.annotation.Handler;
055import org.jgrapes.core.events.Error;
056import org.jgrapes.core.events.Start;
057import org.jgrapes.core.events.Stop;
058import org.jgrapes.io.NioHandler;
059import org.jgrapes.io.events.Close;
060import org.jgrapes.io.events.Closed;
061import org.jgrapes.io.events.IOError;
062import org.jgrapes.io.events.Input;
063import org.jgrapes.io.events.NioRegistration;
064import org.jgrapes.io.events.NioRegistration.Registration;
065import org.jgrapes.io.events.Opening;
066import org.jgrapes.io.events.Output;
067import org.jgrapes.io.events.Purge;
068import org.jgrapes.io.util.AvailabilityListener;
069import org.jgrapes.io.util.LinkedIOSubchannel;
070import org.jgrapes.io.util.PermitsPool;
071import org.jgrapes.net.events.Accepted;
072import org.jgrapes.net.events.Ready;
073import org.jgrapes.util.events.ConfigurationUpdate;
074
075/**
076 * Provides a socket server. The server binds to the given address. If the
077 * address is {@code null}, address and port are automatically assigned.
078 * The port may be overwritten by a configuration event
079 * (see {@link #onConfigurationUpdate(ConfigurationUpdate)}).
080 * 
081 * For each established connection, the server creates a new
082 * {@link LinkedIOSubchannel}. The servers basic operation is to
083 * fire {@link Input} (and {@link Closed}) events on the
084 * appropriate subchannel in response to data received from the
085 * network and to handle {@link Output} (and {@link Close}) events 
086 * on the subchannel and forward the information to the network
087 * connection.
088 * 
089 * The server supports limiting the number of concurrent connections
090 * with a {@link PermitsPool}. If such a pool is set as connection
091 * limiter (see {@link #setConnectionLimiter(PermitsPool)}), a
092 * permit is acquired for each new connection attempt. If no more
093 * permits are available, the server sends a {@link Purge} event on
094 * each channel that is purgeable for at least the time span
095 * set with {@link #setMinimalPurgeableTime(long)}. Purgeability 
096 * is derived from the end of record flag of {@link Output} events
097 * (see {@link #onOutput(Output, SocketChannelImpl)}. When using this feature, 
098 * make sure that connections are either short lived or the application
099 * level components support the {@link Purge} event. Else, it may become
100 * impossible to establish new connections.
101 */
102@SuppressWarnings({ "PMD.ExcessiveImports", "PMD.ExcessivePublicCount",
103    "PMD.NcssCount", "PMD.EmptyCatchBlock", "PMD.AvoidDuplicateLiterals",
104    "PMD.ExcessiveClassLength", "PMD.CouplingBetweenObjects" })
105public class SocketServer extends SocketConnectionManager
106        implements NioHandler {
107
108    private SocketAddress serverAddress;
109    private ServerSocketChannel serverSocketChannel;
110    private boolean closing;
111    private int backlog;
112    private PermitsPool connLimiter;
113    private Registration registration;
114    @SuppressWarnings("PMD.SingularField")
115    private Purger purger;
116    private long minimumPurgeableTime;
117
118    /**
119     * The purger thread.
120     */
121    private class Purger extends Thread implements AvailabilityListener {
122
123        private boolean permitsAvailable = true;
124
125        /**
126         * Instantiates a new purger.
127         */
128        public Purger() {
129            setName(Components.simpleObjectName(this));
130            setDaemon(true);
131        }
132
133        @Override
134        public void availabilityChanged(PermitsPool pool, boolean available) {
135            if (registration == null) {
136                return;
137            }
138            synchronized (this) {
139                permitsAvailable = available;
140                registration.updateInterested(
141                    permitsAvailable ? SelectionKey.OP_ACCEPT : 0);
142                if (!permitsAvailable) {
143                    this.notifyAll();
144                }
145            }
146        }
147
148        @Override
149        @SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops",
150            "PMD.DataflowAnomalyAnalysis", "PMD.CognitiveComplexity" })
151        public void run() {
152            if (connLimiter == null) {
153                return;
154            }
155            try {
156                connLimiter.addListener(this);
157                while (serverSocketChannel.isOpen()) {
158                    synchronized (this) {
159                        while (permitsAvailable) {
160                            wait();
161                        }
162                    }
163                    // Copy to avoid ConcurrentModificationException
164                    List<SocketChannelImpl> candidates;
165                    synchronized (channels) {
166                        candidates = new ArrayList<>(channels);
167                    }
168                    long purgeableSince
169                        = System.currentTimeMillis() - minimumPurgeableTime;
170                    candidates = candidates.stream()
171                        .filter(channel -> channel.isPurgeable()
172                            && channel.purgeableSince() < purgeableSince)
173                        .sorted(new Comparator<>() {
174                            @Override
175                            @SuppressWarnings("PMD.ShortVariable")
176                            public int compare(SocketChannelImpl c1,
177                                    SocketChannelImpl c2) {
178                                if (c1.purgeableSince() < c2
179                                    .purgeableSince()) {
180                                    return 1;
181                                }
182                                if (c1.purgeableSince() > c2
183                                    .purgeableSince()) {
184                                    return -1;
185                                }
186                                return 0;
187                            }
188                        })
189                        .collect(Collectors.toList());
190                    for (SocketChannelImpl channel : candidates) {
191                        // Sorting may have taken time...
192                        if (!channel.isPurgeable()) {
193                            continue;
194                        }
195                        channel.downPipeline().fire(new Purge(), channel);
196                        // Continue only as long as necessary
197                        if (permitsAvailable) {
198                            break;
199                        }
200                    }
201                    sleep(1000);
202                }
203            } catch (InterruptedException e) {
204                // Fall through
205            } finally {
206                connLimiter.removeListener(this);
207            }
208        }
209
210    }
211
212    /**
213     * Creates a new server, using itself as component channel. 
214     */
215    public SocketServer() {
216        this(Channel.SELF);
217    }
218
219    /**
220     * Creates a new server using the given channel.
221     * 
222     * @param componentChannel the component's channel
223     */
224    public SocketServer(Channel componentChannel) {
225        super(componentChannel);
226    }
227
228    /**
229     * Sets the address to bind to. If none is set, the address and port
230     * are assigned automatically.
231     * 
232     * @param serverAddress the address to bind to
233     * @return the socket server for easy chaining
234     */
235    public SocketServer setServerAddress(SocketAddress serverAddress) {
236        this.serverAddress = serverAddress;
237        return this;
238    }
239
240    @Override
241    public SocketServer setBufferSize(int size) {
242        super.setBufferSize(size);
243        return this;
244    }
245
246    /**
247     * The component can be configured with events that include
248     * a path (see @link {@link ConfigurationUpdate#paths()})
249     * that matches this components path (see {@link Manager#componentPath()}).
250     * 
251     * The following properties are recognized:
252     * 
253     * `hostname`
254     * : If given, is used as first parameter for 
255     *   {@link InetSocketAddress#InetSocketAddress(String, int)}.
256     * 
257     * `port`
258     * : If given, is used as parameter for 
259     *   {@link InetSocketAddress#InetSocketAddress(String, int)} 
260     *   or {@link InetSocketAddress#InetSocketAddress(int)}, 
261     *   depending on whether a host name is specified. Defaults to "0".
262     *   
263     * `backlog`
264     * : See {@link #setBacklog(int)}.
265     * 
266     * `bufferSize`
267     * : See {@link #setBufferSize(int)}.
268     * 
269     * `maxConnections`
270     * : Calls {@link #setConnectionLimiter} with a
271     *   {@link PermitsPool} of the specified size.
272     * 
273     * `minimalPurgeableTime`
274     * : See {@link #setMinimalPurgeableTime(long)}.
275     * 
276     * @param event the event
277     */
278    @Handler
279    @SuppressWarnings("PMD.ConfusingTernary")
280    public void onConfigurationUpdate(ConfigurationUpdate event) {
281        event.values(componentPath()).ifPresent(values -> {
282            String hostname = values.get("hostname");
283            if (hostname != null) {
284                setServerAddress(new InetSocketAddress(hostname,
285                    Integer.parseInt(values.getOrDefault("port", "0"))));
286            } else if (values.containsKey("port")) {
287                setServerAddress(new InetSocketAddress(
288                    Integer.parseInt(values.get("port"))));
289            }
290            Optional.ofNullable(values.get("backlog")).ifPresent(
291                value -> setBacklog(Integer.parseInt(value)));
292            Optional.ofNullable(values.get("bufferSize")).ifPresent(
293                value -> setBufferSize(Integer.parseInt(value)));
294            Optional.ofNullable(values.get("maxConnections"))
295                .map(Integer::parseInt).map(PermitsPool::new)
296                .ifPresent(this::setConnectionLimiter);
297            Optional.ofNullable(values.get("minimalPurgeableTime"))
298                .map(Long::parseLong).ifPresent(this::setMinimalPurgeableTime);
299        });
300    }
301
302    /**
303     * Returns the server address. Before starting, the address is the
304     * address set with {@link #setServerAddress(InetSocketAddress)}. After
305     * starting the address is obtained from the created socket.  
306     * 
307     * @return the serverAddress
308     */
309    public SocketAddress serverAddress() {
310        try {
311            return serverSocketChannel == null ? serverAddress
312                : serverSocketChannel.getLocalAddress();
313        } catch (IOException e) {
314            return serverAddress;
315        }
316    }
317
318    /**
319     * Sets the backlog size.
320     * 
321     * @param backlog the backlog to set
322     * @return the socket server for easy chaining
323     */
324    public SocketServer setBacklog(int backlog) {
325        this.backlog = backlog;
326        return this;
327    }
328
329    /**
330     * Return the configured backlog size.
331     *
332     * @return the backlog
333     */
334    public int backlog() {
335        return backlog;
336    }
337
338    /**
339     * Sets a permit "pool". A new connection is created only if a permit
340     * can be obtained from the pool. 
341     * 
342     * A connection limiter must be set before starting the component.
343     * 
344     * @param connectionLimiter the connection pool to set
345     * @return the socket server for easy chaining
346     */
347    public SocketServer setConnectionLimiter(PermitsPool connectionLimiter) {
348        this.connLimiter = connectionLimiter;
349        return this;
350    }
351
352    /**
353     * Returns the connection limiter.
354     *
355     * @return the connection Limiter
356     */
357    public PermitsPool getConnectionLimiter() {
358        return connLimiter;
359    }
360
361    /**
362     * Sets a minimal time that a connection must be purgeable (idle)
363     * before it may be purged.
364     *
365     * @param millis the millis
366     * @return the socket server
367     */
368    public SocketServer setMinimalPurgeableTime(long millis) {
369        this.minimumPurgeableTime = millis;
370        return this;
371    }
372
373    /**
374     * Gets the minimal purgeable time.
375     *
376     * @return the minimal purgeable time
377     */
378    public long getMinimalPurgeableTime() {
379        return minimumPurgeableTime;
380    }
381
382    /**
383     * Starts the server.
384     * 
385     * @param event the start event
386     * @throws IOException if an I/O exception occurred
387     */
388    @Handler
389    public void onStart(Start event) throws IOException {
390        closing = false;
391        if (serverAddress instanceof UnixDomainSocketAddress) {
392            serverSocketChannel
393                = ServerSocketChannel.open(StandardProtocolFamily.UNIX);
394        } else {
395            serverSocketChannel = ServerSocketChannel.open();
396        }
397        serverSocketChannel.bind(serverAddress, backlog);
398        MBeanView.addServer(this);
399        fire(new NioRegistration(this, serverSocketChannel,
400            SelectionKey.OP_ACCEPT, this), Channel.BROADCAST);
401    }
402
403    /**
404     * Handles the successful channel registration.
405     *
406     * @param event the event
407     * @throws InterruptedException the interrupted exception
408     * @throws IOException Signals that an I/O exception has occurred.
409     */
410    @Handler(channels = Self.class)
411    public void onRegistered(NioRegistration.Completed event)
412            throws InterruptedException, IOException {
413        NioHandler handler = event.event().handler();
414        if (handler == this) {
415            if (event.event().get() == null) {
416                fire(new Error(event,
417                    "Registration failed, no NioDispatcher?"));
418                return;
419            }
420            registration = event.event().get();
421            purger = new Purger();
422            purger.start();
423            fire(new Ready(serverSocketChannel.getLocalAddress()));
424            return;
425        }
426        if (handler instanceof SocketChannelImpl channel
427            && channels.contains(channel)) {
428            var accepted = new Accepted(channel.nioChannel().getLocalAddress(),
429                channel.nioChannel().getRemoteAddress(), false,
430                Collections.emptyList());
431            var registration = event.event().get();
432            // (1) Opening, (2) Accepted, (3) process input
433            channel.downPipeline().fire(Event.onCompletion(new Opening<Void>(),
434                e -> {
435                    channel.downPipeline().fire(accepted, channel);
436                    channel.registrationComplete(registration);
437                }), channel);
438        }
439    }
440
441    /*
442     * (non-Javadoc)
443     * 
444     * @see org.jgrapes.io.NioSelectable#handleOps(int)
445     */
446    @Override
447    public void handleOps(int ops) {
448        if ((ops & SelectionKey.OP_ACCEPT) == 0 || closing) {
449            return;
450        }
451        synchronized (channels) {
452            if (connLimiter != null && !connLimiter.tryAcquire()) {
453                return;
454            }
455            try {
456                @SuppressWarnings("PMD.CloseResource")
457                SocketChannel socketChannel = serverSocketChannel.accept();
458                if (socketChannel == null) {
459                    // "False alarm"
460                    if (connLimiter != null) {
461                        connLimiter.release();
462                    }
463                    return;
464                }
465                new SocketChannelImpl(null, socketChannel);
466            } catch (IOException e) {
467                fire(new IOError(null, e));
468            }
469        }
470    }
471
472    @Override
473    protected boolean removeChannel(SocketChannelImpl channel) {
474        synchronized (channels) {
475            if (!channels.remove(channel)) {
476                // Closed already
477                return false;
478            }
479            // In case the server is shutting down
480            channels.notifyAll();
481        }
482        if (connLimiter != null) {
483            connLimiter.release();
484        }
485        return true;
486    }
487
488    /**
489     * Shuts down the server or one of the connections to the server.
490     *
491     * @param event the event
492     * @throws IOException if an I/O exception occurred
493     * @throws InterruptedException if the execution was interrupted
494     */
495    @Handler
496    @SuppressWarnings("PMD.DataflowAnomalyAnalysis")
497    public void onClose(Close event) throws IOException, InterruptedException {
498        boolean closeServer = false;
499        for (Channel channel : event.channels()) {
500            if (channels.contains(channel)) {
501                ((SocketChannelImpl) channel).close();
502                continue;
503            }
504            if (channel instanceof Subchannel) {
505                // Some subchannel that we're not interested in.
506                continue;
507            }
508            // Close event on "main" channel
509            closeServer = true;
510        }
511        if (!closeServer) {
512            // Only connection(s) were to be closed.
513            return;
514        }
515        if (!serverSocketChannel.isOpen()) {
516            // Closed already
517            fire(new Closed<Void>());
518            return;
519        }
520        synchronized (channels) {
521            closing = true;
522            // Copy to avoid concurrent modification exception
523            Set<SocketChannelImpl> conns = new HashSet<>(channels);
524            for (SocketChannelImpl conn : conns) {
525                conn.close();
526            }
527            while (!channels.isEmpty()) {
528                channels.wait();
529            }
530        }
531        serverSocketChannel.close();
532        purger.interrupt();
533        closing = false;
534        fire(new Closed<Void>());
535    }
536
537    /**
538     * Shuts down the server by firing a {@link Close} using the
539     * server as channel. Note that this automatically results
540     * in closing all open connections by the runtime system
541     * and thus in {@link Closed} events on all subchannels.
542     * 
543     * @param event the event
544     * @throws InterruptedException 
545     */
546    @Handler(priority = -1000)
547    public void onStop(Stop event) throws InterruptedException {
548        if (closing || !serverSocketChannel.isOpen()) {
549            return;
550        }
551        newEventPipeline().fire(new Close(), this).get();
552    }
553
554    /**
555     * The Interface of the SocketServer MXBean.
556     */
557    public interface SocketServerMXBean {
558
559        /**
560         * The Class ChannelInfo.
561         */
562        class ChannelInfo {
563
564            private final SocketChannelImpl channel;
565
566            /**
567             * Instantiates a new channel info.
568             *
569             * @param channel the channel
570             */
571            public ChannelInfo(SocketChannelImpl channel) {
572                this.channel = channel;
573            }
574
575            /**
576             * Checks if is purgeable.
577             *
578             * @return true, if is purgeable
579             */
580            public boolean isPurgeable() {
581                return channel.isPurgeable();
582            }
583
584            /**
585             * Gets the downstream pool.
586             *
587             * @return the downstream pool
588             */
589            public String getDownstreamPool() {
590                return channel.readBuffers().name();
591            }
592
593            /**
594             * Gets the upstream pool.
595             *
596             * @return the upstream pool
597             */
598            public String getUpstreamPool() {
599                return channel.byteBufferPool().name();
600            }
601        }
602
603        /**
604         * Gets the component path.
605         *
606         * @return the component path
607         */
608        String getComponentPath();
609
610        /**
611         * Gets the channel count.
612         *
613         * @return the channel count
614         */
615        int getChannelCount();
616
617        /**
618         * Gets the channels.
619         *
620         * @return the channels
621         */
622        SortedMap<String, ChannelInfo> getChannels();
623
624    }
625
626    /**
627     * The Class SocketServerInfo.
628     */
629    public static class SocketServerInfo implements SocketServerMXBean {
630
631        private static MBeanServer mbs
632            = ManagementFactory.getPlatformMBeanServer();
633
634        private ObjectName mbeanName;
635        private final WeakReference<SocketServer> serverRef;
636
637        /**
638         * Instantiates a new socket server info.
639         *
640         * @param server the server
641         */
642        @SuppressWarnings({ "PMD.EmptyCatchBlock",
643            "PMD.AvoidCatchingGenericException",
644            "PMD.ConstructorCallsOverridableMethod" })
645        public SocketServerInfo(SocketServer server) {
646            serverRef = new WeakReference<>(server);
647            try {
648                String endPoint = "";
649                if (server.serverAddress instanceof InetSocketAddress addr) {
650                    endPoint = " (" + addr.getHostName() + ":" + addr.getPort()
651                        + ")";
652                } else if (server.serverAddress instanceof UnixDomainSocketAddress addr) {
653                    endPoint = " (" + addr.getPath() + ")";
654                }
655                mbeanName = new ObjectName("org.jgrapes.io:type="
656                    + SocketServer.class.getSimpleName() + ",name="
657                    + ObjectName
658                        .quote(Components.objectName(server) + endPoint));
659            } catch (MalformedObjectNameException e) {
660                // Should not happen
661            }
662            try {
663                mbs.unregisterMBean(mbeanName);
664            } catch (Exception e) {
665                // Just in case, should not work
666            }
667            try {
668                mbs.registerMBean(this, mbeanName);
669            } catch (InstanceAlreadyExistsException | MBeanRegistrationException
670                    | NotCompliantMBeanException e) {
671                // Have to live with that
672            }
673        }
674
675        /**
676         * Server.
677         *
678         * @return the optional
679         */
680        @SuppressWarnings({ "PMD.AvoidCatchingGenericException",
681            "PMD.EmptyCatchBlock" })
682        public Optional<SocketServer> server() {
683            SocketServer server = serverRef.get();
684            if (server == null) {
685                try {
686                    mbs.unregisterMBean(mbeanName);
687                } catch (Exception e) {
688                    // Should work.
689                }
690            }
691            return Optional.ofNullable(server);
692        }
693
694        @Override
695        public String getComponentPath() {
696            return server().map(mgr -> mgr.componentPath()).orElse("<removed>");
697        }
698
699        @Override
700        public int getChannelCount() {
701            return server().map(server -> server.channels.size()).orElse(0);
702        }
703
704        @Override
705        @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
706        public SortedMap<String, ChannelInfo> getChannels() {
707            return server().map(server -> {
708                SortedMap<String, ChannelInfo> result = new TreeMap<>();
709                for (SocketChannelImpl channel : server.channels) {
710                    result.put(channel.nioChannel().socket()
711                        .getRemoteSocketAddress().toString(),
712                        new ChannelInfo(channel));
713                }
714                return result;
715            }).orElse(Collections.emptySortedMap());
716        }
717    }
718
719    /**
720     * An MBean interface for getting information about the socket servers
721     * and established connections.
722     */
723    public interface SocketServerSummaryMXBean {
724
725        /**
726         * Gets the connections per server statistics.
727         *
728         * @return the connections per server statistics
729         */
730        IntSummaryStatistics getConnectionsPerServerStatistics();
731
732        /**
733         * Gets the servers.
734         *
735         * @return the servers
736         */
737        Set<SocketServerMXBean> getServers();
738    }
739
740    /**
741     * The MBeanView.
742     */
743    private static final class MBeanView implements SocketServerSummaryMXBean {
744        private static Set<SocketServerInfo> serverInfos = new HashSet<>();
745
746        /**
747         * Adds the server to the reported servers.
748         *
749         * @param server the server
750         */
751        public static void addServer(SocketServer server) {
752            synchronized (serverInfos) {
753                serverInfos.add(new SocketServerInfo(server));
754            }
755        }
756
757        /**
758         * Returns the infos.
759         *
760         * @return the sets the
761         */
762        private Set<SocketServerInfo> infos() {
763            Set<SocketServerInfo> expired = new HashSet<>();
764            synchronized (serverInfos) {
765                for (SocketServerInfo serverInfo : serverInfos) {
766                    if (!serverInfo.server().isPresent()) {
767                        expired.add(serverInfo);
768                    }
769                }
770                serverInfos.removeAll(expired);
771            }
772            return serverInfos;
773        }
774
775        @SuppressWarnings("unchecked")
776        @Override
777        public Set<SocketServerMXBean> getServers() {
778            return (Set<SocketServerMXBean>) (Object) infos();
779        }
780
781        @Override
782        public IntSummaryStatistics getConnectionsPerServerStatistics() {
783            return infos().stream().map(info -> info.server().get())
784                .filter(ref -> ref != null).collect(
785                    Collectors.summarizingInt(srv -> srv.channels.size()));
786        }
787    }
788
789    static {
790        try {
791            MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
792            ObjectName mxbeanName = new ObjectName("org.jgrapes.io:type="
793                + SocketServer.class.getSimpleName() + "s");
794            mbs.registerMBean(new MBeanView(), mxbeanName);
795        } catch (MalformedObjectNameException | InstanceAlreadyExistsException
796                | MBeanRegistrationException | NotCompliantMBeanException e) {
797            // Does not happen
798        }
799    }
800
801}