001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
002 *
003 * Copyright © 2019 microBean™.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
014 * implied.  See the License for the specific language governing
015 * permissions and limitations under the License.
016 */
017package org.microbean.jersey.netty.cdi;
018
019import java.lang.annotation.Annotation;
020
021import java.net.URI;
022import java.net.URISyntaxException;
023
024import java.nio.channels.spi.SelectorProvider;
025
026import java.util.ArrayList;
027import java.util.Collection;
028import java.util.HashMap;
029import java.util.Map;
030import java.util.Objects;
031import java.util.Set;
032
033import java.util.concurrent.CountDownLatch;
034import java.util.concurrent.Executor;
035
036import javax.enterprise.context.ApplicationScoped;
037import javax.enterprise.context.BeforeDestroyed;
038import javax.enterprise.context.Initialized;
039
040import javax.enterprise.event.Observes;
041
042import javax.enterprise.inject.Any;
043import javax.enterprise.inject.Default;
044import javax.enterprise.inject.Instance;
045
046import javax.enterprise.inject.spi.Bean;
047import javax.enterprise.inject.spi.BeanManager;
048import javax.enterprise.inject.spi.DeploymentException;
049import javax.enterprise.inject.spi.Extension;
050
051import javax.enterprise.util.TypeLiteral;
052
053import javax.inject.Named;
054
055import javax.ws.rs.ApplicationPath;
056
057import javax.ws.rs.core.Application;
058
059import io.netty.bootstrap.ServerBootstrap;
060import io.netty.bootstrap.ServerBootstrapConfig;
061
062import io.netty.channel.ChannelFactory;
063import io.netty.channel.DefaultSelectStrategyFactory;
064import io.netty.channel.EventLoopGroup;
065import io.netty.channel.SelectStrategyFactory;
066import io.netty.channel.ServerChannel;
067
068import io.netty.channel.nio.NioEventLoopGroup;
069
070import io.netty.channel.socket.nio.NioServerSocketChannel;
071
072import io.netty.handler.ssl.SslContext;
073
074import io.netty.util.concurrent.EventExecutorGroup;
075import io.netty.util.concurrent.Future;
076import io.netty.util.concurrent.RejectedExecutionHandler;
077import io.netty.util.concurrent.RejectedExecutionHandlers;
078import io.netty.util.concurrent.EventExecutorChooserFactory;
079import io.netty.util.concurrent.DefaultEventExecutorChooserFactory;
080
081import org.glassfish.jersey.server.ApplicationHandler;
082
083import org.microbean.configuration.api.Configurations;
084
085import org.microbean.jaxrs.cdi.JaxRsExtension;
086
087import org.microbean.jersey.netty.JerseyChannelInitializer;
088
089/**
090 * A CDI {@linkplain Extension portable extension} that effectively
091 * puts a <a href="https://netty.io/"
092 * target="_parent">Netty</a>-fronted <a
093 * href="https://jersey.github.io/" target="_parent">Jersey</a>
094 * container inside the {@linkplain
095 * javax.enterprise.inject.se.SeContainer CDI container}.
096 *
097 * @author <a href="https://about.me/lairdnelson"
098 * target="_parent">Laird Nelson</a>
099 */
100public class JerseyNettyExtension implements Extension {
101
102
103  /*
104   * Static fields.
105   */
106
107
108  private static final Annotation[] EMPTY_ANNOTATION_ARRAY = new Annotation[0];
109
110
111  /*
112   * Instance fields.
113   */
114
115
116  private final Collection<Throwable> shutdownProblems;
117
118  private volatile Collection<EventExecutorGroup> eventExecutorGroups;
119
120  private volatile CountDownLatch bindLatch;
121
122  private volatile CountDownLatch runLatch;
123
124  private volatile CountDownLatch shutdownLatch;
125
126
127  /*
128   * Constructors.
129   */
130
131
132  /**
133   * Creates a new {@link JerseyNettyExtension}.
134   */
135  public JerseyNettyExtension() {
136    super();
137    this.shutdownProblems = new ArrayList<>();
138    final Thread containerThread = Thread.currentThread();
139    Runtime.getRuntime().addShutdownHook(new Thread(() -> {
140          zeroOut(this.runLatch);
141          zeroOut(this.bindLatch);
142          containerThread.interrupt();
143          try {
144            containerThread.join();
145          } catch (final InterruptedException interruptedException) {
146            Thread.currentThread().interrupt();
147          }
148    }));
149  }
150
151
152  /*
153   * Instance methods.
154   */
155
156
157  // TODO: prioritize
158  private final void onStartup(@Observes @Initialized(ApplicationScoped.class)
159                               final Object event,
160                               final BeanManager beanManager)
161    throws URISyntaxException {
162    if (beanManager != null) {
163      final JaxRsExtension extension = beanManager.getExtension(JaxRsExtension.class);
164      if (extension != null) {
165        final Set<Set<Annotation>> applicationQualifierSets = extension.getAllApplicationQualifiers();
166        if (applicationQualifierSets != null && !applicationQualifierSets.isEmpty()) {
167
168          final Instance<Object> instance = beanManager.createInstance();
169          assert instance != null;
170
171          final Configurations configurations = instance.select(Configurations.class).get();
172
173          final Map<String, String> baseConfigurationCoordinates = configurations.getConfigurationCoordinates();
174
175          final int size = applicationQualifierSets.size();
176          assert size > 0;
177          this.shutdownLatch = new CountDownLatch(size);
178          this.bindLatch = new CountDownLatch(size);
179          final Collection<Throwable> bindProblems = new ArrayList<>();
180
181          for (final Set<Annotation> applicationQualifiers : applicationQualifierSets) {
182
183            // Quick check to bail out if someone CTRL-Ced and caused
184            // Weld's shutdown hook to fire.  That hook fires
185            // a @BeforeDestroyed event on a Thread that is not this
186            // Thread, so it's possible to be processing
187            // an @Initialized(ApplicationScoped.class) event on the
188            // container thread while also processing
189            // a @BeforeDestroyed(ApplicationScoped.class) event.  We
190            // basically want to skip bootstrapping a bunch of things
191            // if we're going down anyway.
192            //
193            // Be particularly mindful later on of the state of the
194            // latches.
195            if (Thread.currentThread().isInterrupted()) {
196              zeroOut(this.bindLatch);
197              this.bindLatch = null;
198              break;
199            }
200
201            final Annotation[] applicationQualifiersArray;
202            if (applicationQualifiers == null) {
203              applicationQualifiersArray = null;
204            } else if (applicationQualifiers.isEmpty()) {
205              applicationQualifiersArray = EMPTY_ANNOTATION_ARRAY;
206            } else {
207              applicationQualifiersArray = applicationQualifiers.toArray(new Annotation[applicationQualifiers.size()]);
208            }
209
210            final Set<Bean<?>> applicationBeans = beanManager.getBeans(Application.class, applicationQualifiersArray);
211            assert applicationBeans != null;
212            assert !applicationBeans.isEmpty();
213
214            try {
215
216              @SuppressWarnings("unchecked")
217              final Bean<Application> applicationBean = (Bean<Application>)beanManager.resolve(applicationBeans);
218              assert applicationBean != null;
219
220              // TODO: need to somehow squirrel away creationalContext
221              // and release it after the application goes out of
222              // scope; kind of doesn't really matter because
223              // Applications are long-lived but still.
224              final Application application =
225                (Application)beanManager.getReference(applicationBean,
226                                                      Application.class,
227                                                      beanManager.createCreationalContext(applicationBean));
228              assert application != null;
229
230              final ApplicationPath applicationPathAnnotation = application.getClass().getAnnotation(ApplicationPath.class);
231              final String applicationPath;
232              if (applicationPathAnnotation == null) {
233                applicationPath = "/";
234              } else {
235                applicationPath = applicationPathAnnotation.value();
236              }
237              assert applicationPath != null;
238
239              final ServerBootstrap serverBootstrap = getServerBootstrap(beanManager, instance, applicationQualifiersArray, true);
240              assert serverBootstrap != null;
241
242              final SslContext sslContext = getSslContext(beanManager, instance, applicationQualifiersArray, true);
243
244              final Map<String, String> qualifierCoordinates = toConfigurationCoordinates(applicationQualifiers);
245              final Map<String, String> configurationCoordinates;
246              if (baseConfigurationCoordinates == null || baseConfigurationCoordinates.isEmpty()) {
247                if (qualifierCoordinates == null || qualifierCoordinates.isEmpty()) {
248                  configurationCoordinates = baseConfigurationCoordinates;
249                } else {
250                  configurationCoordinates = qualifierCoordinates;
251                }
252              } else if (qualifierCoordinates == null || qualifierCoordinates.isEmpty()) {
253                configurationCoordinates = baseConfigurationCoordinates;
254              } else {
255                configurationCoordinates = new HashMap<>(baseConfigurationCoordinates);
256                configurationCoordinates.putAll(qualifierCoordinates);
257              }
258
259              final URI baseUri;
260              if (sslContext == null) {
261                baseUri = new URI("http",
262                                  null /* no userInfo */,
263                                  configurations.getValue(configurationCoordinates, "host", "0.0.0.0"),
264                                  configurations.getValue(configurationCoordinates, "port", Integer.TYPE, "8080"),
265                                  applicationPath,
266                                  null /* no query */,
267                                  null /* no fragment */);
268              } else {
269                baseUri = new URI("https",
270                                  null /* no userInfo */,
271                                  configurations.getValue(configurationCoordinates, "host", "0.0.0.0"),
272                                  configurations.getValue(configurationCoordinates, "port", Integer.TYPE, "443"),
273                                  applicationPath,
274                                  null /* no query */,
275                                  null /* no fragment */);
276              }
277              assert baseUri != null;
278
279              serverBootstrap.childHandler(new JerseyChannelInitializer(baseUri,
280                                                                        sslContext,
281                                                                        new ApplicationHandler(application)));
282              serverBootstrap.validate();
283
284              final ServerBootstrapConfig config = serverBootstrap.config();
285              assert config != null;
286
287              final EventLoopGroup group = config.group();
288              assert group != null; // see validate() above
289              group.terminationFuture()
290                .addListener(f -> {
291                    try {
292                      if (!f.isSuccess()) {
293                        final Throwable throwable = f.cause();
294                        if (throwable != null) {
295                          synchronized (this.shutdownProblems) {
296                            this.shutdownProblems.add(throwable);
297                          }
298                        }
299                      }
300                    } finally {
301                      this.shutdownLatch.countDown();
302                    }
303                  });
304
305              Collection<EventExecutorGroup> eventExecutorGroups = this.eventExecutorGroups;
306              if (eventExecutorGroups == null) {
307                eventExecutorGroups = new ArrayList<>();
308                this.eventExecutorGroups = eventExecutorGroups;
309              }
310              synchronized (eventExecutorGroups) {
311                eventExecutorGroups.add(group);
312              }
313
314              final Future<?> bindFuture;
315              if (config.localAddress() == null) {
316                bindFuture = serverBootstrap.bind(baseUri.getHost(), baseUri.getPort());
317              } else {
318                bindFuture = serverBootstrap.bind();
319              }
320              bindFuture.addListener(f -> {
321                    try {
322                      if (!f.isSuccess()) {
323                        final Throwable throwable = f.cause();
324                        if (throwable != null) {
325                          synchronized (bindProblems) {
326                            bindProblems.add(throwable);
327                          }
328                        }
329                      }
330                    } finally {
331                      final CountDownLatch latch = this.bindLatch;
332                      if (latch != null) {
333                        latch.countDown();
334                      }
335                    }
336                  });
337
338            } catch (final RuntimeException | URISyntaxException throwMe) {
339              zeroOut(this.bindLatch);
340              zeroOut(this.shutdownLatch);
341              synchronized (bindProblems) {
342                for (final Throwable bindProblem : bindProblems) {
343                  throwMe.addSuppressed(bindProblem);
344                }
345              }
346              throw throwMe;
347            }
348
349          }
350
351          final CountDownLatch bindLatch = this.bindLatch;
352          if (bindLatch != null) {
353            try {
354              bindLatch.await();
355            } catch (final InterruptedException interruptedException) {
356              Thread.currentThread().interrupt();
357            }
358            assert bindLatch.getCount() <= 0;
359            this.bindLatch = null;
360          }
361
362          DeploymentException throwMe = null;
363          synchronized (bindProblems) {
364            for (final Throwable bindProblem : bindProblems) {
365              if (throwMe == null) {
366                throwMe = new DeploymentException(bindProblem);
367              } else {
368                throwMe.addSuppressed(bindProblem);
369              }
370            }
371            bindProblems.clear();
372          }
373          if (throwMe != null) {
374            zeroOut(this.shutdownLatch);
375            throw throwMe;
376          }
377
378          this.runLatch = new CountDownLatch(1);
379
380        }
381      }
382    }
383    assert this.bindLatch == null;
384  }
385
386  private final void waitForAllServersToStop(@Observes @BeforeDestroyed(ApplicationScoped.class)
387                                             final Object event) {
388
389    // Note: somewhat interestingly, Weld can fire a @BeforeDestroyed
390    // event on a thread that is not the container thread: a shutdown
391    // hook that it installs.  So we have to take care to be thread
392    // safe.
393
394    final CountDownLatch runLatch = this.runLatch;
395    if (runLatch != null) {
396      try {
397        runLatch.await();
398      } catch (final InterruptedException interruptedException) {
399        Thread.currentThread().interrupt();
400      }
401      assert runLatch.getCount() <= 0;
402      this.runLatch = null;
403    }
404
405    final Collection<EventExecutorGroup> eventExecutorGroups = this.eventExecutorGroups;
406    if (eventExecutorGroups != null) {
407      synchronized (eventExecutorGroups) {
408        for (final EventExecutorGroup group : eventExecutorGroups) {
409          // idempotent
410          group.shutdownGracefully();
411        }
412        eventExecutorGroups.clear();
413      }
414      this.eventExecutorGroups = null;
415    }
416
417    final CountDownLatch shutdownLatch = this.shutdownLatch;
418    if (shutdownLatch != null) {
419      try {
420        shutdownLatch.await();
421      } catch (final InterruptedException interruptedException) {
422        Thread.currentThread().interrupt();
423      }
424      assert shutdownLatch.getCount() <= 0;
425      this.shutdownLatch = null;
426    }
427
428    DeploymentException throwMe = null;
429    synchronized (this.shutdownProblems) {
430      for (final Throwable shutdownProblem : this.shutdownProblems) {
431        if (throwMe == null) {
432          throwMe = new DeploymentException(shutdownProblem);
433        } else {
434          throwMe.addSuppressed(shutdownProblem);
435        }
436      }
437      this.shutdownProblems.clear();
438    }
439    if (throwMe != null) {
440      throw throwMe;
441    }
442
443  }
444
445
446  /*
447   * Production and lookup methods.
448   */
449
450
451  private static final SslContext getSslContext(final BeanManager beanManager,
452                                                          final Instance<Object> instance,
453                                                          final Annotation[] qualifiersArray,
454                                                          final boolean lookup) {
455    return acquire(beanManager,
456                   instance,
457                   SslContext.class,
458                   qualifiersArray,
459                   lookup,
460                   true,
461                   (bm, i, qa) -> null);
462  }
463
464  private static final ServerBootstrap getServerBootstrap(final BeanManager beanManager,
465                                                          final Instance<Object> instance,
466                                                          final Annotation[] qualifiersArray,
467                                                          final boolean lookup) {
468    return acquire(beanManager,
469                   instance,
470                   ServerBootstrap.class,
471                   qualifiersArray,
472                   lookup,
473                   true,
474                   (bm, i, qa) -> {
475                     final ServerBootstrap returnValue = new ServerBootstrap();
476                     // See https://stackoverflow.com/a/28342821/208288
477                     returnValue.group(getEventLoopGroup(bm, i, qa, true));
478                     returnValue.channelFactory(getChannelFactory(bm, i, qa, true));
479
480                     // Permit arbitrary customization
481                     beanManager.getEvent().select(ServerBootstrap.class, qualifiersArray).fire(returnValue);
482                     return returnValue;
483                   });
484  }
485
486  private static final ChannelFactory<? extends ServerChannel> getChannelFactory(final BeanManager beanManager,
487                                                                                 final Instance<Object> instance,
488                                                                                 final Annotation[] qualifiersArray,
489                                                                                 final boolean lookup) {
490    return acquire(beanManager,
491                   instance,
492                   new TypeLiteral<ChannelFactory<? extends ServerChannel>>() {
493                     private static final long serialVersionUID = 1L;
494                   },
495                   qualifiersArray,
496                   lookup,
497                   true,
498                   (bm, i, qa) -> {
499                     final SelectorProvider selectorProvider = getSelectorProvider(bm, i, qa, true);
500                     assert selectorProvider != null;
501                     return () -> new NioServerSocketChannel(selectorProvider);
502                   });
503  }
504
505  private static final EventLoopGroup getEventLoopGroup(final BeanManager beanManager,
506                                                        final Instance<Object> instance,
507                                                        final Annotation[] qualifiersArray,
508                                                        final boolean lookup) {
509    return acquire(beanManager,
510                   instance,
511                   EventLoopGroup.class,
512                   qualifiersArray,
513                   lookup,
514                   true,
515                   (bm, i, qa) -> {
516                     final EventLoopGroup returnValue =
517                       new NioEventLoopGroup(0 /* 0 == default number of threads */,
518                                             getExecutor(bm, i, qa, true), // null is OK
519                                             getEventExecutorChooserFactory(bm, i, qa, true),
520                                             getSelectorProvider(bm, i, qa, true),
521                                             getSelectStrategyFactory(bm, i, qa, true),
522                                             getRejectedExecutionHandler(bm, i, qa, true));
523                     // Permit arbitrary customization.  (Not much you can do here
524                     // except call setIoRatio(int).)
525                     beanManager.getEvent().select(EventLoopGroup.class, qa).fire(returnValue);
526                     return returnValue;
527                   });
528  }
529
530  private static final Executor getExecutor(final BeanManager beanManager,
531                                            final Instance<Object> instance,
532                                            final Annotation[] qualifiersArray,
533                                            final boolean lookup) {
534    return acquire(beanManager,
535                   instance,
536                   Executor.class,
537                   qualifiersArray,
538                   lookup,
539                   false, // do not fall back to @Default one
540                   (bm, i, qa) -> null);
541  }
542
543  private static final RejectedExecutionHandler getRejectedExecutionHandler(final BeanManager beanManager,
544                                                                            final Instance<Object> instance,
545                                                                            final Annotation[] qualifiersArray,
546                                                                            final boolean lookup) {
547    return acquire(beanManager,
548                   instance,
549                   RejectedExecutionHandler.class,
550                   qualifiersArray,
551                   lookup,
552                   true,
553                   (bm, i, qa) -> RejectedExecutionHandlers.reject());
554  }
555
556  private static final SelectorProvider getSelectorProvider(final BeanManager beanManager,
557                                                            final Instance<Object> instance,
558                                                            final Annotation[] qualifiersArray,
559                                                            final boolean lookup) {
560    return acquire(beanManager,
561                   instance,
562                   SelectorProvider.class,
563                   qualifiersArray,
564                   lookup,
565                   true,
566                   (bm, i, qa) -> SelectorProvider.provider());
567  }
568
569  private static final SelectStrategyFactory getSelectStrategyFactory(final BeanManager beanManager,
570                                                                      final Instance<Object> instance,
571                                                                      final Annotation[] qualifiersArray,
572                                                                      final boolean lookup) {
573    return acquire(beanManager,
574                   instance,
575                   SelectStrategyFactory.class,
576                   qualifiersArray,
577                   lookup,
578                   true,
579                   (bm, i, qa) -> DefaultSelectStrategyFactory.INSTANCE);
580  }
581
582  private static final EventExecutorChooserFactory getEventExecutorChooserFactory(final BeanManager beanManager,
583                                                                                  final Instance<Object> instance,
584                                                                                  final Annotation[] qualifiersArray,
585                                                                                  final boolean lookup) {
586    return acquire(beanManager,
587                   instance,
588                   EventExecutorChooserFactory.class,
589                   qualifiersArray,
590                   lookup,
591                   true,
592                   (bm, i, qa) -> DefaultEventExecutorChooserFactory.INSTANCE);
593  }
594
595
596  /*
597   * Static utility methods.
598   */
599
600
601  private static final <T> T acquire(final BeanManager beanManager,
602                                     final Instance<Object> instance,
603                                     final TypeLiteral<T> typeLiteral,
604                                     final Annotation[] qualifiersArray,
605                                     final boolean lookup,
606                                     final boolean fallbackWithDefaultQualifier,
607                                     final DefaultValueFunction<? extends T> defaultValueFunction) {
608    Objects.requireNonNull(beanManager);
609    Objects.requireNonNull(instance);
610    Objects.requireNonNull(typeLiteral);
611    Objects.requireNonNull(defaultValueFunction);
612
613    final T returnValue;
614    final Instance<? extends T> tInstance;
615    if (lookup) {
616      if (qualifiersArray == null || qualifiersArray.length <= 0 || (qualifiersArray.length == 1 && qualifiersArray[0] instanceof Default)) {
617        tInstance = instance.select(typeLiteral);
618      } else {
619        Instance<? extends T> temp = instance.select(typeLiteral, qualifiersArray);
620        if (fallbackWithDefaultQualifier && (temp == null || temp.isUnsatisfied())) {
621          temp = instance.select(typeLiteral);
622        }
623        tInstance = temp;
624      }
625    } else {
626      tInstance = null;
627    }
628    if (tInstance == null || tInstance.isUnsatisfied()) {
629      returnValue = defaultValueFunction.getDefaultValue(beanManager, instance, qualifiersArray);
630    } else {
631      returnValue = tInstance.get();
632    }
633    return returnValue;
634  }
635
636  private static final <T> T acquire(final BeanManager beanManager,
637                                     final Instance<Object> instance,
638                                     final Class<T> cls,
639                                     final Annotation[] qualifiersArray,
640                                     final boolean lookup,
641                                     final boolean fallbackWithDefaultQualifier,
642                                     final DefaultValueFunction<? extends T> defaultValueFunction) {
643    Objects.requireNonNull(beanManager);
644    Objects.requireNonNull(instance);
645    Objects.requireNonNull(cls);
646    Objects.requireNonNull(defaultValueFunction);
647
648    final T returnValue;
649    final Instance<? extends T> tInstance;
650    if (lookup) {
651      if (qualifiersArray == null || qualifiersArray.length <= 0 || (qualifiersArray.length == 1 && qualifiersArray[0] instanceof Default)) {
652        tInstance = instance.select(cls);
653      } else {
654        Instance<? extends T> temp = instance.select(cls, qualifiersArray);
655        if (fallbackWithDefaultQualifier && (temp == null || temp.isUnsatisfied())) {
656          temp = instance.select(cls);
657        }
658        tInstance = temp;
659      }
660    } else {
661      tInstance = null;
662    }
663    if (tInstance == null || tInstance.isUnsatisfied()) {
664      returnValue = defaultValueFunction.getDefaultValue(beanManager, instance, qualifiersArray);
665    } else {
666      returnValue = tInstance.get();
667    }
668    return returnValue;
669  }
670
671  private static final void zeroOut(final CountDownLatch latch) {
672    if (latch != null) {
673      while (latch.getCount() > 0L) {
674        latch.countDown();
675      }
676      assert latch.getCount() == 0L;
677    }
678  }
679
680  private static final Map<String, String> toConfigurationCoordinates(final Set<? extends Annotation> qualifiers) {
681    final Map<String, String> returnValue = new HashMap<>();
682    if (qualifiers != null && !qualifiers.isEmpty()) {
683      for (final Annotation qualifier : qualifiers) {
684        if (qualifier instanceof Named) {
685          returnValue.put("name", ((Named)qualifier).value());
686        } else if (!(qualifier instanceof Default) && !(qualifier instanceof Any)) {
687          returnValue.put(qualifier.toString(), "");
688        }
689      }
690    }
691    return returnValue;
692  }
693
694
695  /*
696   * Inner and nested classes.
697   */
698
699
700  @FunctionalInterface
701  private static interface DefaultValueFunction<T> {
702
703    T getDefaultValue(final BeanManager beanManager,
704                      final Instance<Object> instance,
705                      final Annotation[] qualifiersArray);
706
707  }
708
709}