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            var accepted = new Accepted(channel.nioChannel().getLocalAddress(),
428                channel.nioChannel().getRemoteAddress(), false,
429                Collections.emptyList());
430            var registration = event.event().get();
431            // (1) Opening, (2) Accepted, (3) process input
432            channel.downPipeline().fire(Event.onCompletion(new Opening<Void>(),
433                e -> {
434                    channel.downPipeline().fire(accepted, channel);
435                    channel.registrationComplete(registration);
436                }), channel);
437        }
438    }
439
440    /*
441     * (non-Javadoc)
442     * 
443     * @see org.jgrapes.io.NioSelectable#handleOps(int)
444     */
445    @Override
446    public void handleOps(int ops) {
447        if ((ops & SelectionKey.OP_ACCEPT) == 0 || closing) {
448            return;
449        }
450        synchronized (channels) {
451            if (connLimiter != null && !connLimiter.tryAcquire()) {
452                return;
453            }
454            try {
455                @SuppressWarnings("PMD.CloseResource")
456                SocketChannel socketChannel = serverSocketChannel.accept();
457                if (socketChannel == null) {
458                    // "False alarm"
459                    if (connLimiter != null) {
460                        connLimiter.release();
461                    }
462                    return;
463                }
464                channels.add(new SocketChannelImpl(null, socketChannel));
465            } catch (IOException e) {
466                fire(new IOError(null, e));
467            }
468        }
469    }
470
471    @Override
472    protected boolean removeChannel(SocketChannelImpl channel) {
473        synchronized (channels) {
474            if (!channels.remove(channel)) {
475                // Closed already
476                return false;
477            }
478            // In case the server is shutting down
479            channels.notifyAll();
480        }
481        if (connLimiter != null) {
482            connLimiter.release();
483        }
484        return true;
485    }
486
487    /**
488     * Shuts down the server or one of the connections to the server.
489     *
490     * @param event the event
491     * @throws IOException if an I/O exception occurred
492     * @throws InterruptedException if the execution was interrupted
493     */
494    @Handler
495    @SuppressWarnings("PMD.DataflowAnomalyAnalysis")
496    public void onClose(Close event) throws IOException, InterruptedException {
497        boolean closeServer = false;
498        for (Channel channel : event.channels()) {
499            if (channels.contains(channel)) {
500                ((SocketChannelImpl) channel).close();
501                continue;
502            }
503            if (channel instanceof Subchannel) {
504                // Some subchannel that we're not interested in.
505                continue;
506            }
507            // Close event on "main" channel
508            closeServer = true;
509        }
510        if (!closeServer) {
511            // Only connection(s) were to be closed.
512            return;
513        }
514        if (!serverSocketChannel.isOpen()) {
515            // Closed already
516            fire(new Closed<Void>());
517            return;
518        }
519        synchronized (channels) {
520            closing = true;
521            // Copy to avoid concurrent modification exception
522            Set<SocketChannelImpl> conns = new HashSet<>(channels);
523            for (SocketChannelImpl conn : conns) {
524                conn.close();
525            }
526            while (!channels.isEmpty()) {
527                channels.wait();
528            }
529        }
530        serverSocketChannel.close();
531        purger.interrupt();
532        closing = false;
533        fire(new Closed<Void>());
534    }
535
536    /**
537     * Shuts down the server by firing a {@link Close} using the
538     * server as channel. Note that this automatically results
539     * in closing all open connections by the runtime system
540     * and thus in {@link Closed} events on all subchannels.
541     * 
542     * @param event the event
543     * @throws InterruptedException 
544     */
545    @Handler(priority = -1000)
546    public void onStop(Stop event) throws InterruptedException {
547        if (closing || !serverSocketChannel.isOpen()) {
548            return;
549        }
550        newEventPipeline().fire(new Close(), this).get();
551    }
552
553    /**
554     * The Interface of the SocketServer MXBean.
555     */
556    public interface SocketServerMXBean {
557
558        /**
559         * The Class ChannelInfo.
560         */
561        class ChannelInfo {
562
563            private final SocketChannelImpl channel;
564
565            /**
566             * Instantiates a new channel info.
567             *
568             * @param channel the channel
569             */
570            public ChannelInfo(SocketChannelImpl channel) {
571                this.channel = channel;
572            }
573
574            /**
575             * Checks if is purgeable.
576             *
577             * @return true, if is purgeable
578             */
579            public boolean isPurgeable() {
580                return channel.isPurgeable();
581            }
582
583            /**
584             * Gets the downstream pool.
585             *
586             * @return the downstream pool
587             */
588            public String getDownstreamPool() {
589                return channel.readBuffers().name();
590            }
591
592            /**
593             * Gets the upstream pool.
594             *
595             * @return the upstream pool
596             */
597            public String getUpstreamPool() {
598                return channel.byteBufferPool().name();
599            }
600        }
601
602        /**
603         * Gets the component path.
604         *
605         * @return the component path
606         */
607        String getComponentPath();
608
609        /**
610         * Gets the channel count.
611         *
612         * @return the channel count
613         */
614        int getChannelCount();
615
616        /**
617         * Gets the channels.
618         *
619         * @return the channels
620         */
621        SortedMap<String, ChannelInfo> getChannels();
622
623    }
624
625    /**
626     * The Class SocketServerInfo.
627     */
628    public static class SocketServerInfo implements SocketServerMXBean {
629
630        private static MBeanServer mbs
631            = ManagementFactory.getPlatformMBeanServer();
632
633        private ObjectName mbeanName;
634        private final WeakReference<SocketServer> serverRef;
635
636        /**
637         * Instantiates a new socket server info.
638         *
639         * @param server the server
640         */
641        @SuppressWarnings({ "PMD.EmptyCatchBlock",
642            "PMD.AvoidCatchingGenericException",
643            "PMD.ConstructorCallsOverridableMethod" })
644        public SocketServerInfo(SocketServer server) {
645            serverRef = new WeakReference<>(server);
646            try {
647                String endPoint = "";
648                if (server.serverAddress instanceof InetSocketAddress addr) {
649                    endPoint = " (" + addr.getHostName() + ":" + addr.getPort()
650                        + ")";
651                } else if (server.serverAddress instanceof UnixDomainSocketAddress addr) {
652                    endPoint = " (" + addr.getPath() + ")";
653                }
654                mbeanName = new ObjectName("org.jgrapes.io:type="
655                    + SocketServer.class.getSimpleName() + ",name="
656                    + ObjectName
657                        .quote(Components.objectName(server) + endPoint));
658            } catch (MalformedObjectNameException e) {
659                // Should not happen
660            }
661            try {
662                mbs.unregisterMBean(mbeanName);
663            } catch (Exception e) {
664                // Just in case, should not work
665            }
666            try {
667                mbs.registerMBean(this, mbeanName);
668            } catch (InstanceAlreadyExistsException | MBeanRegistrationException
669                    | NotCompliantMBeanException e) {
670                // Have to live with that
671            }
672        }
673
674        /**
675         * Server.
676         *
677         * @return the optional
678         */
679        @SuppressWarnings({ "PMD.AvoidCatchingGenericException",
680            "PMD.EmptyCatchBlock" })
681        public Optional<SocketServer> server() {
682            SocketServer server = serverRef.get();
683            if (server == null) {
684                try {
685                    mbs.unregisterMBean(mbeanName);
686                } catch (Exception e) {
687                    // Should work.
688                }
689            }
690            return Optional.ofNullable(server);
691        }
692
693        @Override
694        public String getComponentPath() {
695            return server().map(mgr -> mgr.componentPath()).orElse("<removed>");
696        }
697
698        @Override
699        public int getChannelCount() {
700            return server().map(server -> server.channels.size()).orElse(0);
701        }
702
703        @Override
704        @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
705        public SortedMap<String, ChannelInfo> getChannels() {
706            return server().map(server -> {
707                SortedMap<String, ChannelInfo> result = new TreeMap<>();
708                for (SocketChannelImpl channel : server.channels) {
709                    result.put(channel.nioChannel().socket()
710                        .getRemoteSocketAddress().toString(),
711                        new ChannelInfo(channel));
712                }
713                return result;
714            }).orElse(Collections.emptySortedMap());
715        }
716    }
717
718    /**
719     * An MBean interface for getting information about the socket servers
720     * and established connections.
721     */
722    public interface SocketServerSummaryMXBean {
723
724        /**
725         * Gets the connections per server statistics.
726         *
727         * @return the connections per server statistics
728         */
729        IntSummaryStatistics getConnectionsPerServerStatistics();
730
731        /**
732         * Gets the servers.
733         *
734         * @return the servers
735         */
736        Set<SocketServerMXBean> getServers();
737    }
738
739    /**
740     * The MBeanView.
741     */
742    private static final class MBeanView implements SocketServerSummaryMXBean {
743        private static Set<SocketServerInfo> serverInfos = new HashSet<>();
744
745        /**
746         * Adds the server to the reported servers.
747         *
748         * @param server the server
749         */
750        public static void addServer(SocketServer server) {
751            synchronized (serverInfos) {
752                serverInfos.add(new SocketServerInfo(server));
753            }
754        }
755
756        /**
757         * Returns the infos.
758         *
759         * @return the sets the
760         */
761        private Set<SocketServerInfo> infos() {
762            Set<SocketServerInfo> expired = new HashSet<>();
763            synchronized (serverInfos) {
764                for (SocketServerInfo serverInfo : serverInfos) {
765                    if (!serverInfo.server().isPresent()) {
766                        expired.add(serverInfo);
767                    }
768                }
769                serverInfos.removeAll(expired);
770            }
771            return serverInfos;
772        }
773
774        @SuppressWarnings("unchecked")
775        @Override
776        public Set<SocketServerMXBean> getServers() {
777            return (Set<SocketServerMXBean>) (Object) infos();
778        }
779
780        @Override
781        public IntSummaryStatistics getConnectionsPerServerStatistics() {
782            return infos().stream().map(info -> info.server().get())
783                .filter(ref -> ref != null).collect(
784                    Collectors.summarizingInt(srv -> srv.channels.size()));
785        }
786    }
787
788    static {
789        try {
790            MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
791            ObjectName mxbeanName = new ObjectName("org.jgrapes.io:type="
792                + SocketServer.class.getSimpleName() + "s");
793            mbs.registerMBean(new MBeanView(), mxbeanName);
794        } catch (MalformedObjectNameException | InstanceAlreadyExistsException
795                | MBeanRegistrationException | NotCompliantMBeanException e) {
796            // Does not happen
797        }
798    }
799
800}