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